Skip to content

Commit 7db3d82

Browse files
hejsztynxexploIF
andauthored
feat(web): onLinkPress and onMentionPress functionality (#678)
# Summary Implemented the `onLinkPress` and `onMentionPress` callback functionality. ## Test Plan - write a mention or a link using the input in the example app - press on it - a relevant callback should be visible in the logs ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ❌ | | Web | ✅ | ## Checklist - [x] E2E tests are passing - [x] Required E2E tests have been added (if applicable) --------- Co-authored-by: Igor Furgała <74370735+exploIF@users.noreply.github.com>
1 parent c0a8e88 commit 7db3d82

13 files changed

Lines changed: 602 additions & 57 deletions
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
test.setTimeout(90_000);
4+
5+
const sel = {
6+
root: '[data-testid="test-enriched-text-root"]',
7+
htmlInput: '[data-testid="test-enriched-text-html-input"]',
8+
setValueButton: '[data-testid="test-enriched-text-set-value-button"]',
9+
valueOutput: '[data-testid="test-enriched-text-value-output"]',
10+
display: '[data-testid="test-enriched-text-display"]',
11+
displayInner: '[data-testid="test-enriched-text-display"] .et-view',
12+
linkPressOutput: '[data-testid="test-enriched-text-link-press-output"]',
13+
mentionPressOutput: '[data-testid="test-enriched-text-mention-press-output"]',
14+
} as const;
15+
16+
async function gotoTestEnrichedText(page: Page): Promise<void> {
17+
await page.goto('/test-enriched-text');
18+
await page.waitForSelector(sel.displayInner);
19+
}
20+
21+
async function setEnrichedTextValue(page: Page, html: string): Promise<void> {
22+
await page.fill(sel.htmlInput, html);
23+
await page.click(sel.setValueButton);
24+
25+
await expect
26+
.poll(async () => (await page.locator(sel.valueOutput).textContent()) ?? '')
27+
.toBe(html);
28+
}
29+
30+
function linkPress(page: Page) {
31+
return page.locator(sel.linkPressOutput);
32+
}
33+
34+
function mentionPress(page: Page) {
35+
return page.locator(sel.mentionPressOutput);
36+
}
37+
38+
test.describe('EnrichedText link press', () => {
39+
test('pressing a link emits onLinkPress with the url', async ({ page }) => {
40+
await gotoTestEnrichedText(page);
41+
await setEnrichedTextValue(
42+
page,
43+
'<html><p>Visit <a href="https://example.com">our site</a> now.</p></html>'
44+
);
45+
46+
await expect(linkPress(page)).toHaveText('null');
47+
48+
await page.locator(`${sel.displayInner} a`).click();
49+
50+
await expect
51+
.poll(async () =>
52+
JSON.parse((await linkPress(page).textContent()) || 'null')
53+
)
54+
.toEqual({ url: 'https://example.com' });
55+
});
56+
57+
test('pressing text styled inside a link still emits onLinkPress', async ({
58+
page,
59+
}) => {
60+
await gotoTestEnrichedText(page);
61+
await setEnrichedTextValue(
62+
page,
63+
'<html><p>A bold <a href="https://example.com/path">l<b>in</b>k</a> here.</p></html>'
64+
);
65+
66+
await page.locator(`${sel.displayInner} a b`).click();
67+
68+
await expect
69+
.poll(async () =>
70+
JSON.parse((await linkPress(page).textContent()) || 'null')
71+
)
72+
.toEqual({ url: 'https://example.com/path' });
73+
});
74+
75+
test('pressing non-link text does not emit onLinkPress', async ({ page }) => {
76+
await gotoTestEnrichedText(page);
77+
await setEnrichedTextValue(
78+
page,
79+
'<html><p>Plain text with a <a href="https://example.com">link</a>.</p></html>'
80+
);
81+
82+
await page
83+
.locator(`${sel.displayInner} p`)
84+
.click({ position: { x: 2, y: 2 } });
85+
86+
await expect(linkPress(page)).toHaveText('null');
87+
});
88+
});
89+
90+
test.describe('EnrichedText mention press', () => {
91+
test('pressing a mention emits onMentionPress with text and indicator', async ({
92+
page,
93+
}) => {
94+
await gotoTestEnrichedText(page);
95+
await setEnrichedTextValue(
96+
page,
97+
'<html><p>Hello <mention indicator="@" text="@John Doe">@John Doe</mention>!</p></html>'
98+
);
99+
100+
await expect(mentionPress(page)).toHaveText('null');
101+
102+
await page.locator(`${sel.displayInner} mention`).click();
103+
104+
await expect
105+
.poll(async () =>
106+
JSON.parse((await mentionPress(page).textContent()) || 'null')
107+
)
108+
.toEqual({ text: '@John Doe', indicator: '@', attributes: {} });
109+
});
110+
111+
test('pressing text styled inside a mention still emits onMentionPress', async ({
112+
page,
113+
}) => {
114+
await gotoTestEnrichedText(page);
115+
await setEnrichedTextValue(
116+
page,
117+
'<html><p>Hi <mention indicator="@" text="@Jane">@Ja<s>ne</s></mention>.</p></html>'
118+
);
119+
120+
await page.locator(`${sel.displayInner} mention s`).click();
121+
122+
await expect
123+
.poll(async () =>
124+
JSON.parse((await mentionPress(page).textContent()) || 'null')
125+
)
126+
.toEqual({ text: '@Jane', indicator: '@', attributes: {} });
127+
});
128+
129+
test('pressing non-mention text does not emit onMentionPress', async ({
130+
page,
131+
}) => {
132+
await gotoTestEnrichedText(page);
133+
await setEnrichedTextValue(
134+
page,
135+
'<html><p>Some text <mention indicator="@" text="@Jane">@Jane</mention>.</p></html>'
136+
);
137+
138+
await page
139+
.locator(`${sel.displayInner} p`)
140+
.click({ position: { x: 2, y: 2 } });
141+
142+
await expect(mentionPress(page)).toHaveText('null');
143+
});
144+
});
145+
146+
test.describe('EnrichedText press inside lists', () => {
147+
const listCases = [
148+
{
149+
name: 'unordered list',
150+
open: '<ul>',
151+
close: '</ul>',
152+
},
153+
{
154+
name: 'ordered list',
155+
open: '<ol>',
156+
close: '</ol>',
157+
},
158+
{
159+
name: 'checkbox list',
160+
open: '<ul data-type="checkbox">',
161+
close: '</ul>',
162+
},
163+
];
164+
165+
for (const c of listCases) {
166+
test(`pressing a link inside a ${c.name} emits onLinkPress`, async ({
167+
page,
168+
}) => {
169+
await gotoTestEnrichedText(page);
170+
await setEnrichedTextValue(
171+
page,
172+
`<html>${c.open}<li>Visit <a href="https://example.com/list">our site</a></li><li>Other item</li>${c.close}</html>`
173+
);
174+
175+
await expect(linkPress(page)).toHaveText('null');
176+
177+
await page.locator(`${sel.displayInner} li a`).click();
178+
179+
await expect
180+
.poll(async () =>
181+
JSON.parse((await linkPress(page).textContent()) || 'null')
182+
)
183+
.toEqual({ url: 'https://example.com/list' });
184+
});
185+
186+
test(`pressing a mention inside a ${c.name} emits onMentionPress`, async ({
187+
page,
188+
}) => {
189+
await gotoTestEnrichedText(page);
190+
await setEnrichedTextValue(
191+
page,
192+
`<html>${c.open}<li>Hello <mention indicator="@" text="@John Doe" id="1">@John Doe</mention></li><li>Other item</li>${c.close}</html>`
193+
);
194+
195+
await expect(mentionPress(page)).toHaveText('null');
196+
197+
await page.locator(`${sel.displayInner} li mention`).click();
198+
199+
await expect
200+
.poll(async () =>
201+
JSON.parse((await mentionPress(page).textContent()) || 'null')
202+
)
203+
.toEqual({
204+
text: '@John Doe',
205+
indicator: '@',
206+
attributes: { id: '1' },
207+
});
208+
});
209+
}
210+
});

apps/example-web/src/App.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
type OnChangeMentionEvent,
1616
type OnMentionDetected,
1717
EnrichedText,
18+
type OnLinkPressEvent,
19+
type OnMentionPressEvent,
1820
} from 'react-native-enriched-html';
1921
import { WEB_DEFAULT_HTML_STYLE } from './defaultHtmlStyle';
2022
import type { NativeSyntheticEvent, TextStyle } from 'react-native';
@@ -171,6 +173,14 @@ function App() {
171173
setSelection(e.nativeEvent);
172174
};
173175

176+
const handleLinkPress = (e: OnLinkPressEvent) => {
177+
console.log('[EnrichedText] link press event', e);
178+
};
179+
180+
const handleMentionPress = (e: OnMentionPressEvent) => {
181+
console.log('[EnrichedText] mention press event', e);
182+
};
183+
174184
const openLinkModal = () => {
175185
setIsLinkModalOpen(true);
176186
};
@@ -335,6 +345,8 @@ function App() {
335345
<EnrichedText
336346
style={enrichedTextStyle}
337347
htmlStyle={WEB_DEFAULT_HTML_STYLE}
348+
onLinkPress={handleLinkPress}
349+
onMentionPress={handleMentionPress}
338350
>
339351
{enrichedTextValue}
340352
</EnrichedText>

apps/example-web/src/testScreens/TestEnrichedText.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { useState, type ChangeEvent } from 'react';
2-
import { EnrichedText } from 'react-native-enriched-html';
2+
import {
3+
EnrichedText,
4+
type OnLinkPressEvent,
5+
type OnMentionPressEvent,
6+
} from 'react-native-enriched-html';
37
import type { TextStyle } from 'react-native';
48
import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle';
59

@@ -8,6 +12,11 @@ const INITIAL_VALUE = '<html><p></p></html>';
812
export function TestEnrichedText() {
913
const [htmlInput, setHtmlInput] = useState(INITIAL_VALUE);
1014
const [value, setValue] = useState(INITIAL_VALUE);
15+
const [lastLinkPress, setLastLinkPress] = useState<OnLinkPressEvent | null>(
16+
null
17+
);
18+
const [lastMentionPress, setLastMentionPress] =
19+
useState<OnMentionPressEvent | null>(null);
1120
const [isWide, setIsWide] = useState(false);
1221

1322
return (
@@ -19,6 +28,8 @@ export function TestEnrichedText() {
1928
<EnrichedText
2029
style={isWide ? enrichedTextWideStyle : enrichedTextStyle}
2130
htmlStyle={WEB_DEFAULT_HTML_STYLE}
31+
onLinkPress={setLastLinkPress}
32+
onMentionPress={setLastMentionPress}
2233
>
2334
{value}
2435
</EnrichedText>
@@ -53,6 +64,13 @@ export function TestEnrichedText() {
5364
</button>
5465

5566
<pre data-testid="test-enriched-text-value-output">{value}</pre>
67+
68+
<pre data-testid="test-enriched-text-link-press-output">
69+
{JSON.stringify(lastLinkPress)}
70+
</pre>
71+
<pre data-testid="test-enriched-text-mention-press-output">
72+
{JSON.stringify(lastMentionPress)}
73+
</pre>
5674
</div>
5775
);
5876
}

docs/TEXT_API_REFERENCE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ interface OnLinkPressEvent {
9494
}
9595
```
9696

97-
| Type | Default Value | Platform |
98-
| ----------------------------------- | ------------- | ------------ |
99-
| `(event: OnLinkPressEvent) => void` | - | iOS, Android |
97+
| Type | Default Value | Platform |
98+
| ----------------------------------- | ------------- | ----------------- |
99+
| `(event: OnLinkPressEvent) => void` | - | iOS, Android, Web |
100100

101101
### `onMentionPress`
102102

@@ -110,9 +110,9 @@ interface OnMentionPressEvent {
110110
}
111111
```
112112

113-
| Type | Default Value | Platform |
114-
| -------------------------------------- | ------------- | ------------ |
115-
| `(event: OnMentionPressEvent) => void` | - | iOS, Android |
113+
| Type | Default Value | Platform |
114+
| -------------------------------------- | ------------- | ----------------- |
115+
| `(event: OnMentionPressEvent) => void` | - | iOS, Android, Web |
116116

117117
## EnrichedTextHtmlStyle type
118118

docs/WEB.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ See [Web Keyboard Shortcuts](./INPUT_API_REFERENCE.md#web-keyboard-shortcuts) fo
3939
### What works
4040

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

4344
### Unsupported
4445

4546
- **`selectable`**: ignored on web.
4647
- **`useHtmlNormalizer`**: ignored on web.
4748
- **`ellipsizeMode`**: ignored on web.
4849
- **`numberOfLines`**: ignored on web.
49-
- **Press events**: `onLinkPress` and `onMentionPress` callbacks are ignored on web.
5050

5151
## HTML sanitization
5252

src/web/EnrichedText.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@
116116
.et-view a {
117117
color: var(--et-link-color);
118118
text-decoration-line: var(--et-link-text-decoration-line);
119+
transition: none;
120+
}
121+
122+
.et-view a:active {
123+
color: var(--et-link-press-color);
119124
}
120125

121126
.eti-editor li,

0 commit comments

Comments
 (0)