Skip to content

Commit 429f991

Browse files
authored
Merge branch '@ksienkiewicz/docs-core-functionalities' into @ksienkiewicz/docs-guides
2 parents 15342cf + 6560ad1 commit 429f991

47 files changed

Lines changed: 2161 additions & 1441 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
appId: swmansion.enriched.example
2+
---
3+
# Fix: merge inline styles with plain text
4+
- launchApp
5+
6+
- tapOn:
7+
id: 'toggle-screen-button'
8+
9+
- tapOn:
10+
id: 'editor-input'
11+
12+
- tapOn:
13+
id: 'toolbar-bold'
14+
- tapOn:
15+
id: 'toolbar-italic'
16+
- tapOn:
17+
id: 'toolbar-underline'
18+
19+
- inputText: 'First line'
20+
- pressKey: Enter
21+
22+
- tapOn:
23+
id: 'toolbar-bold'
24+
- tapOn:
25+
id: 'toolbar-italic'
26+
- tapOn:
27+
id: 'toolbar-underline'
28+
- inputText: 'Second line'
29+
30+
- tapOn:
31+
id: 'editor-input'
32+
point: '5%,75%'
33+
34+
- pressKey: Backspace
35+
36+
- runFlow:
37+
file: '../subflows/capture_or_assert_screenshot.yaml'
38+
env:
39+
SCREENSHOT_NAME: 'inline_styles_merge'
6.9 KB
Loading
8.9 KB
Loading

.playwright/helpers/visual-regression.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const visualRegressionSelectors = {
77
setValueButton: '[data-testid="visual-regression-set-value-button"]',
88
editorHtmlOutput: '[data-testid="visual-regression-editor-html-output"]',
99
htmlStyleOverride: '[data-testid="visual-regression-html-style-override"]',
10+
textShortcutsOverride: '[data-testid="visual-regression-text-shortcuts"]',
1011
} as const;
1112

1213
export function editorLocator(page: Page): Locator {
@@ -52,3 +53,10 @@ export async function setHtmlStyleOverride(
5253
): Promise<void> {
5354
await page.fill(visualRegressionSelectors.htmlStyleOverride, json);
5455
}
56+
57+
export async function setTextShortcutsOverride(
58+
page: Page,
59+
json: string
60+
): Promise<void> {
61+
await page.fill(visualRegressionSelectors.textShortcutsOverride, json);
62+
}

.playwright/tests/links.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ test.describe('test-links copy-paste', () => {
479479

480480
await setTestLinksEditorHtml(
481481
page,
482-
'<html><p><a href="custom://link">custom://link</a></p></html>'
482+
'<html><p><a href="/custom-link">/custom-link</a></p></html>'
483483
);
484484

485485
await copyWholeContent(editor);
@@ -488,7 +488,7 @@ test.describe('test-links copy-paste', () => {
488488

489489
await expect
490490
.poll(async () => getTestLinksSerializedHtml(page))
491-
.toContain('<a href="custom://link">custom://link</a>');
491+
.toContain('<a href="/custom-link">/custom-link</a>');
492492
});
493493
});
494494

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { test, expect, type Page } from '@playwright/test';
2+
3+
import {
4+
focusEnrichedEditable,
5+
getSerializedHtml,
6+
gotoVisualRegression,
7+
setEditorHtml,
8+
setTextShortcutsOverride,
9+
} from '../helpers/visual-regression';
10+
import { toolbarButton } from '../helpers/toolbar';
11+
12+
const DELAY_MS = 80;
13+
14+
async function typeText(page: Page, text: string): Promise<void> {
15+
const editor = await focusEnrichedEditable(page);
16+
await editor.pressSequentially(text, { delay: DELAY_MS });
17+
}
18+
19+
test.describe('text shortcuts — paragraph (default)', () => {
20+
test.beforeEach(async ({ page }) => {
21+
await gotoVisualRegression(page);
22+
});
23+
24+
test('typing "- " at paragraph start creates an unordered list', async ({
25+
page,
26+
}) => {
27+
await setEditorHtml(page, '<html><p></p></html>');
28+
await typeText(page, '- ');
29+
30+
await expect
31+
.poll(async () => {
32+
const html = await getSerializedHtml(page);
33+
return /<ul/i.test(html) && /<li/i.test(html);
34+
})
35+
.toBe(true);
36+
});
37+
38+
test('typing "1. " at paragraph start creates an ordered list', async ({
39+
page,
40+
}) => {
41+
await setEditorHtml(page, '<html><p></p></html>');
42+
await typeText(page, '1. ');
43+
44+
await expect
45+
.poll(async () => {
46+
const html = await getSerializedHtml(page);
47+
return /<ol/i.test(html) && /<li/i.test(html);
48+
})
49+
.toBe(true);
50+
});
51+
52+
test('typing "- " in the middle of text does not trigger list shortcut', async ({
53+
page,
54+
}) => {
55+
await setEditorHtml(page, '<html><p></p></html>');
56+
// Type some text first so "- " is not at the paragraph start
57+
await typeText(page, 'hello - ');
58+
59+
await expect.poll(async () => getSerializedHtml(page)).toMatch(/hello - /);
60+
61+
const html = await getSerializedHtml(page);
62+
expect(html).not.toMatch(/<ul/i);
63+
});
64+
65+
test('typing "- " inside an existing list does not create a new list', async ({
66+
page,
67+
}) => {
68+
await setEditorHtml(page, '<html><ul><li>existing item</li></ul></html>');
69+
const editor = await focusEnrichedEditable(page);
70+
await editor.press('End');
71+
await editor.pressSequentially('1. ', { delay: DELAY_MS });
72+
73+
const html = await getSerializedHtml(page);
74+
expect(html).not.toMatch(/<ol/i);
75+
});
76+
});
77+
78+
test.describe('text shortcuts — paragraph (custom)', () => {
79+
test.beforeEach(async ({ page }) => {
80+
await gotoVisualRegression(page);
81+
});
82+
83+
test('custom "# " shortcut converts to h1', async ({ page }) => {
84+
await setTextShortcutsOverride(page, '[{"trigger":"# ","style":"h1"}]');
85+
await setEditorHtml(page, '<html><p></p></html>');
86+
await typeText(page, '# ');
87+
88+
await expect.poll(async () => getSerializedHtml(page)).toMatch(/<h1/i);
89+
});
90+
91+
test('custom "> " shortcut converts to blockquote', async ({ page }) => {
92+
await setTextShortcutsOverride(
93+
page,
94+
'[{"trigger":"> ","style":"blockquote"}]'
95+
);
96+
await setEditorHtml(page, '<html><p></p></html>');
97+
await typeText(page, '> ');
98+
99+
await expect
100+
.poll(async () => getSerializedHtml(page))
101+
.toMatch(/<blockquote/i);
102+
});
103+
104+
test('custom paragraph shortcut trigger text is removed from the output', async ({
105+
page,
106+
}) => {
107+
await setTextShortcutsOverride(page, '[{"trigger":"# ","style":"h1"}]');
108+
await setEditorHtml(page, '<html><p></p></html>');
109+
await typeText(page, '# ');
110+
111+
const html = await getSerializedHtml(page);
112+
expect(html).not.toMatch(/# /);
113+
});
114+
115+
test('empty textShortcuts array disables all shortcuts', async ({ page }) => {
116+
await setTextShortcutsOverride(page, '[]');
117+
await setEditorHtml(page, '<html><p></p></html>');
118+
await typeText(page, '- ');
119+
120+
const html = await getSerializedHtml(page);
121+
expect(html).not.toMatch(/<ul/i);
122+
expect(html).toMatch(/- /);
123+
});
124+
});
125+
126+
test.describe('text shortcuts — inline (custom)', () => {
127+
test.beforeEach(async ({ page }) => {
128+
await gotoVisualRegression(page);
129+
});
130+
131+
test('single-char delimiter: "*hello*" applies italic', async ({ page }) => {
132+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
133+
await setEditorHtml(page, '<html><p></p></html>');
134+
await typeText(page, '*hello*');
135+
136+
await expect
137+
.poll(async () => getSerializedHtml(page))
138+
.toMatch(/<i>hello<\/i>/i);
139+
});
140+
141+
test('double-char delimiter: "**hello**" applies bold', async ({ page }) => {
142+
await setTextShortcutsOverride(page, '[{"trigger":"**","style":"bold"}]');
143+
await setEditorHtml(page, '<html><p></p></html>');
144+
await typeText(page, '**hello**');
145+
146+
await expect
147+
.poll(async () => getSerializedHtml(page))
148+
.toMatch(/<b>hello<\/b>/i);
149+
});
150+
151+
test('backtick delimiter: "`hello`" applies inline code', async ({
152+
page,
153+
}) => {
154+
await setTextShortcutsOverride(
155+
page,
156+
'[{"trigger":"`","style":"inline_code"}]'
157+
);
158+
await setEditorHtml(page, '<html><p></p></html>');
159+
await typeText(page, '`hello`');
160+
161+
await expect
162+
.poll(async () => getSerializedHtml(page))
163+
.toMatch(/<code>hello<\/code>/i);
164+
});
165+
166+
test('inline shortcut removes both delimiters from output', async ({
167+
page,
168+
}) => {
169+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
170+
await setEditorHtml(page, '<html><p></p></html>');
171+
await typeText(page, '*hello*');
172+
173+
const html = await getSerializedHtml(page);
174+
expect(html).not.toMatch(/\*/);
175+
});
176+
177+
test('longer trigger takes precedence: "**" does not trigger "*" shortcut', async ({
178+
page,
179+
}) => {
180+
await setTextShortcutsOverride(
181+
page,
182+
'[{"trigger":"*","style":"italic"},{"trigger":"**","style":"bold"}]'
183+
);
184+
await setEditorHtml(page, '<html><p></p></html>');
185+
await typeText(page, '**hello**');
186+
187+
const html = await getSerializedHtml(page);
188+
// Should apply bold, NOT italic
189+
expect(html).toMatch(/<b>hello<\/b>/i);
190+
expect(html).not.toMatch(/<i>/i);
191+
});
192+
193+
test('inline shortcut does not fire when there is no matching opening delimiter', async ({
194+
page,
195+
}) => {
196+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
197+
await setEditorHtml(page, '<html><p></p></html>');
198+
await typeText(page, 'hello*');
199+
200+
const html = await getSerializedHtml(page);
201+
expect(html).not.toMatch(/<i>/i);
202+
expect(html).toMatch(/hello\*/);
203+
});
204+
});
205+
206+
test.describe('text shortcuts — mark clearing after inline shortcut', () => {
207+
test.beforeEach(async ({ page }) => {
208+
await gotoVisualRegression(page);
209+
});
210+
211+
test('text typed immediately after bold shortcut is not bold', async ({
212+
page,
213+
}) => {
214+
await setTextShortcutsOverride(page, '[{"trigger":"**","style":"bold"}]');
215+
await setEditorHtml(page, '<html><p></p></html>');
216+
217+
// Apply bold via shortcut
218+
await typeText(page, '**hello**');
219+
220+
// Type more text right after — it should NOT be bold
221+
const editor = await focusEnrichedEditable(page);
222+
await editor.pressSequentially(' world', { delay: DELAY_MS });
223+
224+
await expect(toolbarButton(page, 'bold')).not.toHaveClass(
225+
/toolbar-btn--active/
226+
);
227+
228+
const html = await getSerializedHtml(page);
229+
// " world" must be outside the <b> tag
230+
expect(html).not.toMatch(/<b>hello world<\/b>/i);
231+
expect(html).toMatch(/<b>hello<\/b>/i);
232+
});
233+
234+
test('text typed immediately after italic shortcut is not italic', async ({
235+
page,
236+
}) => {
237+
await setTextShortcutsOverride(page, '[{"trigger":"*","style":"italic"}]');
238+
await setEditorHtml(page, '<html><p></p></html>');
239+
240+
await typeText(page, '*hello*');
241+
242+
const editor = await focusEnrichedEditable(page);
243+
await editor.pressSequentially(' world', { delay: DELAY_MS });
244+
245+
await expect(toolbarButton(page, 'italic')).not.toHaveClass(
246+
/toolbar-btn--active/
247+
);
248+
249+
const html = await getSerializedHtml(page);
250+
expect(html).not.toMatch(/<i>hello world<\/i>/i);
251+
expect(html).toMatch(/<i>hello<\/i>/i);
252+
});
253+
});

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ We can help you build your next dream product –
4747
## Prerequisites
4848

4949
- `react-native-enriched-html` currently supports Android and iOS, Web implementation is still experimental.
50-
- It works only with [the React Native New Architecture (Fabric)](https://reactnative.dev/architecture/landing-page) and supports following React Native releases: `0.81`, `0.82`, `0.83`, `0.84` and `0.85`.
50+
- It works only with [the React Native New Architecture (Fabric)](https://reactnative.dev/architecture/landing-page) and supports following React Native releases: `0.81`, `0.82`, `0.83`, `0.84`, `0.85` and `0.86`.
5151

5252
## Installation
5353

android/src/main/java/com/swmansion/enriched/textinput/utils/ShortcutsHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ShortcutsHandler(
4646
val resolvedStyle = resolveStyleName(styleName) ?: continue
4747

4848
s.replace(effectiveTriggerStart, effectiveTriggerStart + trigger.length, "")
49-
view.toggleStyle(resolvedStyle)
49+
view.verifyAndToggleStyle(resolvedStyle)
5050
return
5151
}
5252
}

apps/example-web/src/App.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const DEFAULT_LINK_STATE: OnLinkDetected = {
3838
const LINK_REGEX =
3939
/^(?:enriched:\/\/\S+|(?:https?:\/\/)?(?:www\.)?swmansion\.com(?:\/\S*)?)$/i;
4040

41+
const SANITIZATION_CONFIG = {
42+
linkRegex: LINK_REGEX,
43+
};
44+
4145
function App() {
4246
const ref = useRef<EnrichedTextInputInstance>(null);
4347
const [currentHtml, setCurrentHtml] = useState('');
@@ -121,16 +125,16 @@ function App() {
121125

122126
const handleUserMentionSelected = (item: MentionItem) => {
123127
ref.current?.setMention('@', `@${item.name}`, {
124-
id: item.id,
125-
type: 'user',
128+
'id': item.id,
129+
'data-type': 'user',
126130
});
127131
closeUserMentionPopup();
128132
};
129133

130134
const handleChannelMentionSelected = (item: MentionItem) => {
131135
ref.current?.setMention('#', `#${item.name}`, {
132-
id: item.id,
133-
type: 'channel',
136+
'id': item.id,
137+
'data-type': 'channel',
134138
});
135139
closeChannelMentionPopup();
136140
};
@@ -278,6 +282,7 @@ function App() {
278282
mentionIndicators={['@', '#']}
279283
htmlStyle={WEB_DEFAULT_HTML_STYLE}
280284
linkRegex={LINK_REGEX}
285+
sanitizationConfig={SANITIZATION_CONFIG}
281286
/>
282287
<MentionPopup
283288
variant="user"

0 commit comments

Comments
 (0)