Skip to content

Commit 23dc283

Browse files
authored
Merge branch 'main' into @ksienkiewicz/fix-list-styling
2 parents 4784ab8 + 5a733d4 commit 23dc283

44 files changed

Lines changed: 1492 additions & 1259 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.playwright/tests/links.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ const sel = {
2929
selectionStart: '[data-testid="test-links-selection-start"]',
3030
selectionEnd: '[data-testid="test-links-selection-end"]',
3131
applySelection: '[data-testid="test-links-apply-selection-button"]',
32+
applySetLinkFromSelection:
33+
'[data-testid="test-links-apply-setlink-from-selection-button"]',
34+
selectionPayload: '[data-testid="test-links-selection-payload"]',
3235
onLinkDetectedPayload: '[data-testid="on-link-detected-payload"]',
3336
editorInner: '[data-testid="test-links-editor"] .eti-editor',
3437
editorScreenshot: '[data-testid="test-links-editor"]',
@@ -251,6 +254,61 @@ test.describe('test-links setLink table', () => {
251254
}
252255
});
253256

257+
test.describe('test-links setLink round-trips onChangeSelection text', () => {
258+
test('linking a selection across a block boundary keeps both paragraphs', async ({
259+
page,
260+
}) => {
261+
await gotoTestLinks(page);
262+
await setTestLinksEditorHtml(page, '<html><p>hello</p><p>world</p></html>');
263+
264+
await page.fill(sel.selectionStart, '3');
265+
await page.fill(sel.selectionEnd, '8');
266+
await page.fill(sel.setLinkUrl, 'https://swmansion.com');
267+
await page.click(sel.applySelection);
268+
269+
await expect
270+
.poll(async () => page.locator(sel.selectionPayload).textContent())
271+
.toBe(JSON.stringify({ start: 3, end: 8, text: 'lo\nwo' }));
272+
273+
await page.click(sel.applySetLinkFromSelection);
274+
275+
await expect
276+
.poll(async () => getTestLinksSerializedHtml(page))
277+
.toContain(
278+
'<p>hel<a href="https://swmansion.com">lo</a></p>' +
279+
'<p><a href="https://swmansion.com">wo</a>rld</p>'
280+
);
281+
});
282+
283+
test('linking a selection across a block boundary preserves inline marks', async ({
284+
page,
285+
}) => {
286+
await gotoTestLinks(page);
287+
await setTestLinksEditorHtml(
288+
page,
289+
'<html><p>hel<b>lo</b></p><p>world</p></html>'
290+
);
291+
292+
await page.fill(sel.selectionStart, '3');
293+
await page.fill(sel.selectionEnd, '8');
294+
await page.fill(sel.setLinkUrl, 'https://swmansion.com');
295+
await page.click(sel.applySelection);
296+
297+
await expect
298+
.poll(async () => page.locator(sel.selectionPayload).textContent())
299+
.toBe(JSON.stringify({ start: 3, end: 8, text: 'lo\nwo' }));
300+
301+
await page.click(sel.applySetLinkFromSelection);
302+
303+
await expect
304+
.poll(async () => getTestLinksSerializedHtml(page))
305+
.toContain(
306+
'<p>hel<a href="https://swmansion.com"><b>lo</b></a></p>' +
307+
'<p><a href="https://swmansion.com">wo</a>rld</p>'
308+
);
309+
});
310+
});
311+
254312
test.describe('test-links removeLink table', () => {
255313
const cases: {
256314
name: string;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
const ROOT_SELECTOR = '[data-testid="test-render-cycle-root"]';
4+
const EDITOR_SELECTOR = `${ROOT_SELECTOR} .eti-editor [contenteditable="true"]`;
5+
const TOGGLE_BUTTON_SELECTOR = '[data-testid="toggle-variant-button"]';
6+
const VARIANT_OUTPUT_SELECTOR = '[data-testid="variant-output"]';
7+
const PAGE_PATH = '/test-render-cycle';
8+
9+
test.describe('EnrichedTextInput render cycle', () => {
10+
test('does not throw on simultaneous defaultValue and htmlStyle change', async ({
11+
page,
12+
}) => {
13+
const pageErrors: Error[] = [];
14+
const consoleErrors: string[] = [];
15+
16+
page.on('pageerror', (error) => pageErrors.push(error));
17+
page.on('console', (message) => {
18+
if (message.type() === 'error') {
19+
consoleErrors.push(message.text());
20+
}
21+
});
22+
23+
await page.goto(PAGE_PATH);
24+
await page.waitForSelector(EDITOR_SELECTOR);
25+
26+
await expect(page.locator(EDITOR_SELECTOR)).toContainText('Variant A');
27+
28+
await page.click(TOGGLE_BUTTON_SELECTOR);
29+
await expect(page.locator(VARIANT_OUTPUT_SELECTOR)).toHaveText('b');
30+
await expect(page.locator(EDITOR_SELECTOR)).toContainText('Variant B');
31+
32+
await page.click(TOGGLE_BUTTON_SELECTOR);
33+
await expect(page.locator(VARIANT_OUTPUT_SELECTOR)).toHaveText('a');
34+
await expect(page.locator(EDITOR_SELECTOR)).toContainText('Variant A');
35+
36+
const editor = page.locator(EDITOR_SELECTOR);
37+
await editor.click();
38+
await expect(editor).toBeFocused();
39+
await editor.pressSequentially(' more text');
40+
await expect(editor).toContainText('Variant A more text');
41+
42+
expect(pageErrors).toEqual([]);
43+
expect(consoleErrors).toEqual([]);
44+
});
45+
});

android/build.gradle

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ buildscript {
2020

2121

2222
apply plugin: "com.android.library"
23-
apply plugin: "kotlin-android"
23+
// AGP 9 ships built-in Kotlin support and registers the `kotlin` extension
24+
// itself. Applying the Kotlin plugin on top of it fails configuration with
25+
// "Cannot add extension with name 'kotlin'". Only apply it when nothing has
26+
// registered that extension yet.
27+
if (project.extensions.findByName('kotlin') == null) {
28+
apply plugin: "kotlin-android"
29+
}
2430

2531
apply plugin: "com.facebook.react"
2632

android/gradle.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
ReactNativeEnrichedHtml_kotlinVersion=2.0.21
1+
ReactNativeEnrichedHtml_kotlinVersion=2.2.0
22
ReactNativeEnrichedHtml_minSdkVersion=24
3-
ReactNativeEnrichedHtml_targetSdkVersion=34
4-
ReactNativeEnrichedHtml_compileSdkVersion=35
3+
ReactNativeEnrichedHtml_targetSdkVersion=36
4+
ReactNativeEnrichedHtml_compileSdkVersion=37
55
ReactNativeEnrichedHtml_ndkVersion=27.1.12297006

android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewLayoutManager.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ class EnrichedTextInputViewLayoutManager(
88
private var forceHeightRecalculationCounter: Int = 0
99

1010
fun invalidateLayout() {
11+
val stateWrapper = view.stateWrapper ?: return
12+
1113
val text = view.text
1214
val paint = view.paint
1315

1416
val needUpdate = MeasurementStore.store(view.id, text, paint)
1517
if (!needUpdate) return
1618

17-
val counter = forceHeightRecalculationCounter
1819
forceHeightRecalculationCounter++
1920
val state = Arguments.createMap()
20-
state.putInt("forceHeightRecalculationCounter", counter)
21-
view.stateWrapper?.updateState(state)
21+
state.putInt("forceHeightRecalculationCounter", forceHeightRecalculationCounter)
22+
stateWrapper.updateState(state)
2223
}
2324

2425
fun releaseMeasurementStore() {

android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ object MeasurementStore {
130130
props: ReadableMap?,
131131
): Float {
132132
val propsFontSize = props?.getDouble("fontSize")?.toFloat()
133-
if (propsFontSize == null) return defaultView.textSize
133+
if (propsFontSize == null || propsFontSize <= 0) return defaultView.textSize
134134

135135
return ceil(pixelFromSpOrDp(propsFontSize, allowFontScalingFromProps(props)))
136136
}

android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class EnrichedTextInputShadowNode final
4747
const LayoutConstraints &layoutConstraints) const override;
4848

4949
private:
50-
int forceHeightRecalculationCounter_;
50+
int forceHeightRecalculationCounter_{0};
5151
std::shared_ptr<EnrichedTextInputMeasurementManager> measurementsManager_;
5252
};
5353
} // namespace facebook::react

apps/example-web/src/RouteSelector.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { VisualRegression } from './testScreens/VisualRegression';
66
import { TestSubmitProps } from './testScreens/TestSubmitProps';
77
import { TestEnrichedText } from './testScreens/TestEnrichedText';
88
import { TestEllipsize } from './testScreens/TestEllipsize';
9+
import { TestRenderCycle } from './testScreens/TestRenderCycle';
910
import { useEffect, useState } from 'react';
1011

1112
export default function RouteSelector() {
@@ -50,5 +51,9 @@ export default function RouteSelector() {
5051
return <TestEllipsize />;
5152
}
5253

54+
if (path === '/test-render-cycle') {
55+
return <TestRenderCycle />;
56+
}
57+
5358
return <App />;
5459
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
EnrichedTextInput,
44
type EnrichedInputStyle,
55
type EnrichedTextInputInstance,
6+
type OnChangeSelectionEvent,
67
type OnLinkDetected,
78
} from 'react-native-enriched-html';
89
import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle';
@@ -34,6 +35,8 @@ export function TestLinks() {
3435
const [selEndInput, setSelEndInput] = useState('0');
3536
const [lastOnLinkDetected, setLastOnLinkDetected] =
3637
useState<OnLinkDetected | null>(null);
38+
const [lastSelection, setLastSelection] =
39+
useState<OnChangeSelectionEvent | null>(null);
3740

3841
useEffect(() => {
3942
setLinkRegexError('');
@@ -68,6 +71,9 @@ export function TestLinks() {
6871
onLinkDetected={(e) => {
6972
setLastOnLinkDetected(e);
7073
}}
74+
onChangeSelection={(e) => {
75+
setLastSelection(e.nativeEvent);
76+
}}
7177
linkRegex={appliedLinkRegex}
7278
/>
7379
</div>
@@ -223,8 +229,27 @@ export function TestLinks() {
223229
>
224230
setSelection
225231
</button>
232+
<button
233+
type="button"
234+
data-testid="test-links-apply-setlink-from-selection-button"
235+
onClick={() => {
236+
if (!lastSelection) return;
237+
ref.current?.setLink(
238+
lastSelection.start,
239+
lastSelection.end,
240+
lastSelection.text,
241+
linkUrlInput
242+
);
243+
}}
244+
>
245+
setLink from selection
246+
</button>
226247
</div>
227248

249+
<pre data-testid="test-links-selection-payload">
250+
{JSON.stringify(lastSelection)}
251+
</pre>
252+
228253
<pre data-testid="on-link-detected-payload">
229254
{JSON.stringify(lastOnLinkDetected)}
230255
</pre>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { useMemo, useRef, useState } from 'react';
2+
import {
3+
EnrichedTextInput,
4+
type EnrichedTextInputInstance,
5+
type HtmlStyle,
6+
} from 'react-native-enriched-html';
7+
import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle';
8+
9+
const VARIANTS = {
10+
a: {
11+
defaultValue: '<p>Variant A</p>',
12+
htmlStyle: WEB_DEFAULT_HTML_STYLE,
13+
},
14+
b: {
15+
defaultValue: '<h1>Variant B</h1>',
16+
htmlStyle: { ...WEB_DEFAULT_HTML_STYLE, h1: { fontSize: 48 } },
17+
},
18+
} as const satisfies Record<
19+
string,
20+
{ defaultValue: string; htmlStyle: HtmlStyle }
21+
>;
22+
23+
export function TestRenderCycle() {
24+
const ref = useRef<EnrichedTextInputInstance>(null);
25+
const [variant, setVariant] = useState<keyof typeof VARIANTS>('a');
26+
27+
const { defaultValue, htmlStyle } = useMemo(
28+
() => VARIANTS[variant],
29+
[variant]
30+
);
31+
32+
return (
33+
<div data-testid="test-render-cycle-root">
34+
<div
35+
className="editor-wrapper"
36+
style={editorContainerStyle}
37+
data-testid="editor-container"
38+
onClick={() => ref.current?.focus()}
39+
>
40+
<EnrichedTextInput
41+
ref={ref}
42+
defaultValue={defaultValue}
43+
htmlStyle={htmlStyle}
44+
placeholder="Test editor"
45+
autoFocus
46+
editable
47+
scrollEnabled
48+
/>
49+
</div>
50+
51+
<button
52+
type="button"
53+
data-testid="toggle-variant-button"
54+
onClick={() => {
55+
setVariant((prev) => (prev === 'a' ? 'b' : 'a'));
56+
}}
57+
>
58+
Toggle variant
59+
</button>
60+
61+
<pre data-testid="variant-output">{variant}</pre>
62+
</div>
63+
);
64+
}
65+
66+
const editorContainerStyle = {
67+
backgroundColor: '#ddd',
68+
padding: '16px',
69+
borderRadius: '8px',
70+
} as const;

0 commit comments

Comments
 (0)