Skip to content
Open
210 changes: 210 additions & 0 deletions .playwright/tests/enrichedTextPress.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { test, expect, type Page } from '@playwright/test';

test.setTimeout(90_000);

const sel = {
root: '[data-testid="test-enriched-text-root"]',
htmlInput: '[data-testid="test-enriched-text-html-input"]',
setValueButton: '[data-testid="test-enriched-text-set-value-button"]',
valueOutput: '[data-testid="test-enriched-text-value-output"]',
display: '[data-testid="test-enriched-text-display"]',
displayInner: '[data-testid="test-enriched-text-display"] .et-view',
linkPressOutput: '[data-testid="test-enriched-text-link-press-output"]',
mentionPressOutput: '[data-testid="test-enriched-text-mention-press-output"]',
} as const;

async function gotoTestEnrichedText(page: Page): Promise<void> {
await page.goto('/test-enriched-text');
await page.waitForSelector(sel.displayInner);
}

async function setEnrichedTextValue(page: Page, html: string): Promise<void> {
await page.fill(sel.htmlInput, html);
await page.click(sel.setValueButton);

await expect
.poll(async () => (await page.locator(sel.valueOutput).textContent()) ?? '')
.toBe(html);
}

function linkPress(page: Page) {
return page.locator(sel.linkPressOutput);
}

function mentionPress(page: Page) {
return page.locator(sel.mentionPressOutput);
}

test.describe('EnrichedText link press', () => {
test('pressing a link emits onLinkPress with the url', async ({ page }) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
'<html><p>Visit <a href="https://example.com">our site</a> now.</p></html>'
);

await expect(linkPress(page)).toHaveText('null');

await page.locator(`${sel.displayInner} a`).click();

await expect
.poll(async () =>
JSON.parse((await linkPress(page).textContent()) || 'null')
)
.toEqual({ url: 'https://example.com' });
});

test('pressing text styled inside a link still emits onLinkPress', async ({
page,
}) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
'<html><p>A bold <a href="https://example.com/path">l<b>in</b>k</a> here.</p></html>'
);

await page.locator(`${sel.displayInner} a b`).click();

await expect
.poll(async () =>
JSON.parse((await linkPress(page).textContent()) || 'null')
)
.toEqual({ url: 'https://example.com/path' });
});

test('pressing non-link text does not emit onLinkPress', async ({ page }) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
'<html><p>Plain text with a <a href="https://example.com">link</a>.</p></html>'
);

await page
.locator(`${sel.displayInner} p`)
.click({ position: { x: 2, y: 2 } });

await expect(linkPress(page)).toHaveText('null');
});
});

test.describe('EnrichedText mention press', () => {
test('pressing a mention emits onMentionPress with text and indicator', async ({
page,
}) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
'<html><p>Hello <mention indicator="@" text="@John Doe">@John Doe</mention>!</p></html>'
);

await expect(mentionPress(page)).toHaveText('null');

await page.locator(`${sel.displayInner} mention`).click();

await expect
.poll(async () =>
JSON.parse((await mentionPress(page).textContent()) || 'null')
)
.toEqual({ text: '@John Doe', indicator: '@', attributes: {} });
});

test('pressing text styled inside a mention still emits onMentionPress', async ({
page,
}) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
'<html><p>Hi <mention indicator="@" text="@Jane">@Ja<s>ne</s></mention>.</p></html>'
);

await page.locator(`${sel.displayInner} mention s`).click();

await expect
.poll(async () =>
JSON.parse((await mentionPress(page).textContent()) || 'null')
)
.toEqual({ text: '@Jane', indicator: '@', attributes: {} });
});

test('pressing non-mention text does not emit onMentionPress', async ({
page,
}) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
'<html><p>Some text <mention indicator="@" text="@Jane">@Jane</mention>.</p></html>'
);

await page
.locator(`${sel.displayInner} p`)
.click({ position: { x: 2, y: 2 } });

await expect(mentionPress(page)).toHaveText('null');
});
});

test.describe('EnrichedText press inside lists', () => {
const listCases = [
{
name: 'unordered list',
open: '<ul>',
close: '</ul>',
},
{
name: 'ordered list',
open: '<ol>',
close: '</ol>',
},
{
name: 'checkbox list',
open: '<ul data-type="checkbox">',
close: '</ul>',
},
];

for (const c of listCases) {
test(`pressing a link inside a ${c.name} emits onLinkPress`, async ({
page,
}) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
`<html>${c.open}<li>Visit <a href="https://example.com/list">our site</a></li><li>Other item</li>${c.close}</html>`
);

await expect(linkPress(page)).toHaveText('null');

await page.locator(`${sel.displayInner} li a`).click();

await expect
.poll(async () =>
JSON.parse((await linkPress(page).textContent()) || 'null')
)
.toEqual({ url: 'https://example.com/list' });
});

test(`pressing a mention inside a ${c.name} emits onMentionPress`, async ({
page,
}) => {
await gotoTestEnrichedText(page);
await setEnrichedTextValue(
page,
`<html>${c.open}<li>Hello <mention indicator="@" text="@John Doe" id="1">@John Doe</mention></li><li>Other item</li>${c.close}</html>`
);

await expect(mentionPress(page)).toHaveText('null');

await page.locator(`${sel.displayInner} li mention`).click();

await expect
.poll(async () =>
JSON.parse((await mentionPress(page).textContent()) || 'null')
)
.toEqual({
text: '@John Doe',
indicator: '@',
attributes: { id: '1' },
});
});
}
});
12 changes: 12 additions & 0 deletions apps/example-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
type OnChangeMentionEvent,
type OnMentionDetected,
EnrichedText,
type OnLinkPressEvent,
type OnMentionPressEvent,
} from 'react-native-enriched-html';
import { WEB_DEFAULT_HTML_STYLE } from './defaultHtmlStyle';
import type { NativeSyntheticEvent, TextStyle } from 'react-native';
Expand Down Expand Up @@ -173,6 +175,14 @@ function App() {
setSelection(e.nativeEvent);
};

const handleLinkPress = (e: OnLinkPressEvent) => {
console.log('[EnrichedText] link press event', e);
};

const handleMentionPress = (e: OnMentionPressEvent) => {
console.log('[EnrichedText] mention press event', e);
};

const openLinkModal = () => {
setIsLinkModalOpen(true);
};
Expand Down Expand Up @@ -337,6 +347,8 @@ function App() {
<EnrichedText
style={enrichedTextStyle}
htmlStyle={WEB_DEFAULT_HTML_STYLE}
onLinkPress={handleLinkPress}
onMentionPress={handleMentionPress}
>
{enrichedTextValue}
</EnrichedText>
Expand Down
20 changes: 19 additions & 1 deletion apps/example-web/src/testScreens/TestEnrichedText.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useState, type ChangeEvent } from 'react';
import { EnrichedText } from 'react-native-enriched-html';
import {
EnrichedText,
type OnLinkPressEvent,
type OnMentionPressEvent,
} from 'react-native-enriched-html';
import type { TextStyle } from 'react-native';
import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle';

Expand All @@ -8,6 +12,11 @@ const INITIAL_VALUE = '<html><p></p></html>';
export function TestEnrichedText() {
const [htmlInput, setHtmlInput] = useState(INITIAL_VALUE);
const [value, setValue] = useState(INITIAL_VALUE);
const [lastLinkPress, setLastLinkPress] = useState<OnLinkPressEvent | null>(
null
);
const [lastMentionPress, setLastMentionPress] =
useState<OnMentionPressEvent | null>(null);

return (
<div data-testid="test-enriched-text-root">
Expand All @@ -18,6 +27,8 @@ export function TestEnrichedText() {
<EnrichedText
style={enrichedTextStyle}
htmlStyle={WEB_DEFAULT_HTML_STYLE}
onLinkPress={setLastLinkPress}
onMentionPress={setLastMentionPress}
>
{value}
</EnrichedText>
Expand All @@ -42,6 +53,13 @@ export function TestEnrichedText() {
</button>

<pre data-testid="test-enriched-text-value-output">{value}</pre>

<pre data-testid="test-enriched-text-link-press-output">
{JSON.stringify(lastLinkPress)}
</pre>
<pre data-testid="test-enriched-text-mention-press-output">
{JSON.stringify(lastMentionPress)}
</pre>
</div>
);
}
Expand Down
12 changes: 6 additions & 6 deletions docs/TEXT_API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ interface OnLinkPressEvent {
}
```

| Type | Default Value | Platform |
| ----------------------------------- | ------------- | ------------ |
| `(event: OnLinkPressEvent) => void` | - | iOS, Android |
| Type | Default Value | Platform |
| ----------------------------------- | ------------- | ----------------- |
| `(event: OnLinkPressEvent) => void` | - | iOS, Android, Web |

### `onMentionPress`

Expand All @@ -110,9 +110,9 @@ interface OnMentionPressEvent {
}
```

| Type | Default Value | Platform |
| -------------------------------------- | ------------- | ------------ |
| `(event: OnMentionPressEvent) => void` | - | iOS, Android |
| Type | Default Value | Platform |
| -------------------------------------- | ------------- | ----------------- |
| `(event: OnMentionPressEvent) => void` | - | iOS, Android, Web |

## EnrichedTextHtmlStyle type

Expand Down
2 changes: 1 addition & 1 deletion docs/WEB.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ See [Web Keyboard Shortcuts](./INPUT_API_REFERENCE.md#web-keyboard-shortcuts) fo
### What works

- Customizing the styling using props: `style`, `htmlStyle`, `selectionColor`.
- `onLinkPress` and `onMentionPress` callbacks

### Unsupported

- **`selectable`**: ignored on web.
- **`useHtmlNormalizer`**: ignored on web.
- **`ellipsizeMode`**: ignored on web.
- **`numberOfLines`**: ignored on web.
- **Press events**: `onLinkPress` and `onMentionPress` callbacks are ignored on web.

## HTML sanitization

Expand Down
7 changes: 5 additions & 2 deletions src/web/EnrichedText.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@
.et-view a {
color: var(--et-link-color);
text-decoration-line: var(--et-link-text-decoration-line);
transition: none;
}

.et-view a:active {
color: var(--et-link-press-color);
}

.eti-editor ul:not([data-type]),
Expand Down Expand Up @@ -271,7 +276,6 @@
.et-view ul[data-type="checkbox"] > li {
position: relative;
list-style: none;
pointer-events: none;
min-height: max(1lh, var(--et-checkbox-box-size, 24px));
}

Expand All @@ -293,7 +297,6 @@
gap: 0;
margin: 0;
padding: 0;
user-select: none;
}

.et-view img {
Expand Down
Loading
Loading