Skip to content

Commit b5df1a2

Browse files
authored
Merge branch 'main' into @ksienkiewicz/feat-text-alignment-shortcuts
2 parents 118fb27 + 7ce3c4d commit b5df1a2

76 files changed

Lines changed: 2045 additions & 1338 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
-69 Bytes
Loading
8 Bytes
Loading
21 Bytes
Loading

.playwright/tests/images.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ test.describe('images', () => {
168168

169169
for (const key of toolbarOrder) {
170170
const editor = await focusEnrichedEditable(page);
171+
await page.waitForTimeout(10);
171172
await editor.press('Meta+A');
172173
await toolbarButton(page, key).click();
173174
await expect

.playwright/tests/links.spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ 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"]',
36+
onLinkPressEnabled: '[data-testid="test-links-onlinkpress-enabled"]',
37+
onLinkPressPayload: '[data-testid="on-link-press-payload"]',
3338
editorInner: '[data-testid="test-links-editor"] .eti-editor',
3439
editorScreenshot: '[data-testid="test-links-editor"]',
3540
linkRegexMode: '[data-testid="test-links-link-regex-mode"]',
@@ -60,6 +65,10 @@ async function getOnLinkDetectedPayload(page: Page): Promise<string> {
6065
return (await page.locator(sel.onLinkDetectedPayload).textContent()) ?? '';
6166
}
6267

68+
async function getOnLinkPressPayload(page: Page): Promise<string> {
69+
return (await page.locator(sel.onLinkPressPayload).textContent()) ?? '';
70+
}
71+
6372
test('links display visual regression', async ({ page }) => {
6473
await gotoVisualRegression(page);
6574
const html = [
@@ -251,6 +260,61 @@ test.describe('test-links setLink table', () => {
251260
}
252261
});
253262

263+
test.describe('test-links setLink round-trips onChangeSelection text', () => {
264+
test('linking a selection across a block boundary keeps both paragraphs', async ({
265+
page,
266+
}) => {
267+
await gotoTestLinks(page);
268+
await setTestLinksEditorHtml(page, '<html><p>hello</p><p>world</p></html>');
269+
270+
await page.fill(sel.selectionStart, '3');
271+
await page.fill(sel.selectionEnd, '8');
272+
await page.fill(sel.setLinkUrl, 'https://swmansion.com');
273+
await page.click(sel.applySelection);
274+
275+
await expect
276+
.poll(async () => page.locator(sel.selectionPayload).textContent())
277+
.toBe(JSON.stringify({ start: 3, end: 8, text: 'lo\nwo' }));
278+
279+
await page.click(sel.applySetLinkFromSelection);
280+
281+
await expect
282+
.poll(async () => getTestLinksSerializedHtml(page))
283+
.toContain(
284+
'<p>hel<a href="https://swmansion.com">lo</a></p>' +
285+
'<p><a href="https://swmansion.com">wo</a>rld</p>'
286+
);
287+
});
288+
289+
test('linking a selection across a block boundary preserves inline marks', async ({
290+
page,
291+
}) => {
292+
await gotoTestLinks(page);
293+
await setTestLinksEditorHtml(
294+
page,
295+
'<html><p>hel<b>lo</b></p><p>world</p></html>'
296+
);
297+
298+
await page.fill(sel.selectionStart, '3');
299+
await page.fill(sel.selectionEnd, '8');
300+
await page.fill(sel.setLinkUrl, 'https://swmansion.com');
301+
await page.click(sel.applySelection);
302+
303+
await expect
304+
.poll(async () => page.locator(sel.selectionPayload).textContent())
305+
.toBe(JSON.stringify({ start: 3, end: 8, text: 'lo\nwo' }));
306+
307+
await page.click(sel.applySetLinkFromSelection);
308+
309+
await expect
310+
.poll(async () => getTestLinksSerializedHtml(page))
311+
.toContain(
312+
'<p>hel<a href="https://swmansion.com"><b>lo</b></a></p>' +
313+
'<p><a href="https://swmansion.com">wo</a>rld</p>'
314+
);
315+
});
316+
});
317+
254318
test.describe('test-links removeLink table', () => {
255319
const cases: {
256320
name: string;
@@ -360,6 +424,39 @@ test.describe('test-links onLinkDetected', () => {
360424
});
361425
});
362426

427+
test.describe('test-links onLinkPress', () => {
428+
test('clicking a link does nothing when onLinkPress is not provided', async ({
429+
page,
430+
}) => {
431+
await gotoTestLinks(page);
432+
await setTestLinksEditorHtml(
433+
page,
434+
'<html><p><a href="https://example.com">Example</a></p></html>'
435+
);
436+
437+
await page.locator(sel.editorInner).locator('a').click();
438+
439+
await expect(page.locator(sel.onLinkPressPayload)).toHaveText('null');
440+
});
441+
442+
test('clicking a link fires onLinkPress with the url when provided', async ({
443+
page,
444+
}) => {
445+
await gotoTestLinks(page);
446+
await page.check(sel.onLinkPressEnabled);
447+
await setTestLinksEditorHtml(
448+
page,
449+
'<html><p><a href="https://example.com">Example</a></p></html>'
450+
);
451+
452+
await page.locator(sel.editorInner).locator('a').click();
453+
454+
await expect
455+
.poll(async () => getOnLinkPressPayload(page))
456+
.toBe(JSON.stringify({ url: 'https://example.com' }));
457+
});
458+
});
459+
363460
test.describe('test-links autolink', () => {
364461
async function resetEditorAndSetLinkRegexMode(
365462
page: Page,
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/text/EnrichedTextView.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.swmansion.enriched.common.pixelFromSpOrDp
3030
import com.swmansion.enriched.text.spans.EnrichedTextImageSpan
3131
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextClickableSpan
3232
import com.swmansion.enriched.text.spans.interfaces.EnrichedTextSpan
33+
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
3334
import kotlin.math.ceil
3435

3536
class EnrichedTextView : AppCompatTextView {
@@ -41,13 +42,15 @@ class EnrichedTextView : AppCompatTextView {
4142
private var fontWeight: Int = ReactConstants.UNSET
4243
private var fontSize: Float = EnrichedConstants.TEXT_DEFAULT_FONT_SIZE
4344
private var fontSizeRaw: Float? = null
45+
private var lineHeight: Float? = null
4446
private var htmlStyleMap: ReadableMap? = null
4547
var allowFontScaling: Boolean = EnrichedConstants.ALLOW_FONT_SCALING_DEFAULT
4648
set(value) {
4749
if (field == value) return
4850
field = value
4951
fontSizeRaw?.let { setFontSize(it) }
5052
htmlStyleMap?.let { setHtmlStyle(it) }
53+
applyLineSpacing()
5154
}
5255

5356
private var enrichedStyle: EnrichedTextStyle? = null
@@ -185,6 +188,7 @@ class EnrichedTextView : AppCompatTextView {
185188
parsedText = null
186189
this.text = text
187190
}
191+
applyLineSpacing()
188192
}
189193

190194
private fun parseText(
@@ -305,6 +309,33 @@ class EnrichedTextView : AppCompatTextView {
305309
setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInt)
306310
}
307311

312+
fun setLineHeight(height: Float) {
313+
lineHeight = if (height <= 0f) null else height
314+
applyLineSpacing()
315+
}
316+
317+
private fun applyLineSpacing() {
318+
val currentText = text ?: return
319+
val spannable =
320+
currentText as? Spannable ?: SpannableString(currentText)
321+
spannable
322+
.getSpans(0, spannable.length, EnrichedLineHeightSpan::class.java)
323+
.forEach { spannable.removeSpan(it) }
324+
325+
lineHeight?.let {
326+
spannable.setSpan(
327+
EnrichedLineHeightSpan(it, allowFontScaling),
328+
0,
329+
spannable.length,
330+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
331+
)
332+
}
333+
334+
if (spannable !== currentText) {
335+
setText(spannable, BufferType.SPANNABLE)
336+
}
337+
}
338+
308339
fun setFontFamily(family: String?) {
309340
if (family != fontFamily) {
310341
fontFamily = family

android/src/main/java/com/swmansion/enriched/text/EnrichedTextViewManager.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ class EnrichedTextViewManager :
5353
view?.setFontSize(value)
5454
}
5555

56+
override fun setLineHeight(
57+
view: EnrichedTextView?,
58+
value: Float,
59+
) {
60+
view?.setLineHeight(value)
61+
}
62+
5663
override fun setFontFamily(
5764
view: EnrichedTextView?,
5865
value: String?,

0 commit comments

Comments
 (0)