From 946c613223f489ece345576adfe906d92031d1a8 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Fri, 10 Jul 2026 11:21:40 +0200 Subject: [PATCH 1/9] feat: onImagePress --- apps/example-web/src/App.tsx | 6 ++++ src/web/EnrichedText.tsx | 13 +++++-- src/web/usePressInteractions.ts | 64 +++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/web/usePressInteractions.ts diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index 8fa6e035a..dade5ddd8 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -15,6 +15,7 @@ import { type OnChangeMentionEvent, type OnMentionDetected, EnrichedText, + type OnImagePressEvent, } from 'react-native-enriched-html'; import { WEB_DEFAULT_HTML_STYLE } from './defaultHtmlStyle'; import type { NativeSyntheticEvent, TextStyle } from 'react-native'; @@ -173,6 +174,10 @@ function App() { setSelection(e.nativeEvent); }; + const handleImagePress = (e: OnImagePressEvent) => { + console.log('[EnrichedText] image press event', e); + }; + const openLinkModal = () => { setIsLinkModalOpen(true); }; @@ -337,6 +342,7 @@ function App() { {enrichedTextValue} diff --git a/src/web/EnrichedText.tsx b/src/web/EnrichedText.tsx index c26efb589..67c16e6d7 100644 --- a/src/web/EnrichedText.tsx +++ b/src/web/EnrichedText.tsx @@ -14,9 +14,16 @@ import { prepareHtmlForWeb } from './normalization/prepareHtmlForWeb'; import { INLINE_IMAGE_CSS_VARIABLES } from './styleConversion/inlineImageCSSVariables'; import { useImageErrorFallback } from './useImageErrorFallback'; import { usePressInteractions } from './usePressInteractions'; +import { useStableRef } from './useStableRef'; export const EnrichedText = memo( - ({ children, htmlStyle, style, selectionColor }: EnrichedTextProps) => { + ({ + children, + htmlStyle, + style, + selectionColor, + onImagePress, + }: EnrichedTextProps) => { const containerRef = useRef(null); const sanitizedHtml = useMemo(() => sanitizeHtml(children), [children]); @@ -64,7 +71,9 @@ export const EnrichedText = memo( [textStyle, themingStyle, cssVars] ); - usePressInteractions(containerRef); + const onImagePressRef = useStableRef(onImagePress); + + usePressInteractions(containerRef, onImagePressRef); useImageErrorFallback(containerRef); return ( diff --git a/src/web/usePressInteractions.ts b/src/web/usePressInteractions.ts new file mode 100644 index 000000000..94fb550d5 --- /dev/null +++ b/src/web/usePressInteractions.ts @@ -0,0 +1,64 @@ +import { useEffect, type RefObject } from 'react'; +import type { OnImagePressEvent } from '../types'; + +type OnImagePressEventRef = RefObject< + ((event: OnImagePressEvent) => void) | undefined +>; + +type ImageAttributes = { + uri: string; + width: number; + height: number; +}; + +export function usePressInteractions( + containerRef: RefObject, + onImagePressRef: OnImagePressEventRef +) { + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const handleInteraction = (e: MouseEvent) => { + const target = e.target as HTMLElement; + + const image = target.closest('img'); + + if (image && container.contains(image)) { + e.preventDefault(); + + const imageAttributes = parseImageAttributs(image); + + if (imageAttributes) { + onImagePressRef.current?.({ + image: imageAttributes, + }); + } + } + }; + + container.addEventListener('click', handleInteraction); + return () => container.removeEventListener('click', handleInteraction); + }, [containerRef, onImagePressRef]); +} + +function parseImageAttributs(image: HTMLElement): ImageAttributes | undefined { + const uri = image.getAttribute('src'); + const rawWidth = image.getAttribute('width'); + const rawHeight = image.getAttribute('height'); + + if (uri && rawWidth && rawHeight) { + const width = parseInt(rawWidth, 10); + const height = parseInt(rawHeight, 10); + + if (!isNaN(width) && !isNaN(height)) { + return { + uri, + width, + height, + }; + } + } + + return undefined; +} From 7b614da186869fd540288e39954156aa6ef6520c Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Fri, 10 Jul 2026 11:26:47 +0200 Subject: [PATCH 2/9] test: onImagePress tests --- .playwright/tests/enrichedTextPress.spec.ts | 191 ++++++++++++++++++ .../src/testScreens/TestEnrichedText.tsx | 12 +- 2 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 .playwright/tests/enrichedTextPress.spec.ts diff --git a/.playwright/tests/enrichedTextPress.spec.ts b/.playwright/tests/enrichedTextPress.spec.ts new file mode 100644 index 000000000..528631e35 --- /dev/null +++ b/.playwright/tests/enrichedTextPress.spec.ts @@ -0,0 +1,191 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.setTimeout(90_000); + +const IMAGE_ROUTE = '**/pw-e2e-ok.png'; +const IMAGE_SRC = '/pw-e2e-ok.png'; +const BROKEN_IMAGE_ROUTE = '**/pw-e2e-broken.png'; +const BROKEN_IMAGE_SRC = '/pw-e2e-broken.png'; +const PNG_BODY = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64' +); + +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', + imagePressOutput: '[data-testid="test-enriched-text-image-press-output"]', +} as const; + +const VISIBILITY_TIMEOUT_MS = 1_000; + +test.beforeEach(async ({ page }) => { + await page.route(IMAGE_ROUTE, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'image/png', + body: PNG_BODY, + }); + }); + await page.route(BROKEN_IMAGE_ROUTE, (route) => route.abort()); +}); + +async function gotoTestEnrichedText(page: Page): Promise { + await page.goto('/test-enriched-text'); + await page.waitForSelector(sel.displayInner); +} + +async function setEnrichedTextValue(page: Page, html: string): Promise { + await page.fill(sel.htmlInput, html); + await page.click(sel.setValueButton); + + await expect + .poll(async () => (await page.locator(sel.valueOutput).textContent()) ?? '') + .toBe(html); +} + +function imagePress(page: Page) { + return page.locator(sel.imagePressOutput); +} + +test.describe('EnrichedText image press', () => { + test('pressing an image emits onImagePress with the image', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

Look now.

` + ); + + await expect(imagePress(page)).toHaveText('null'); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: IMAGE_SRC, width: 32, height: 32 } }); + }); + + test('pressing an image surrounded by styled text still emits onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

A bold here.

` + ); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: IMAGE_SRC, width: 120, height: 60 } }); + }); + + test('pressing non-image text does not emit onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

Plain text with an .

` + ); + + await page + .locator(`${sel.displayInner} p`) + .click({ position: { x: 2, y: 2 } }); + + await expect(imagePress(page)).toHaveText('null'); + }); + + test('pressing an empty-src placeholder does not emit onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + '

Before after.

' + ); + + await expect(page.locator(`${sel.displayInner} img`)).toBeVisible({ + timeout: VISIBILITY_TIMEOUT_MS, + }); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect(imagePress(page)).toHaveText('null'); + }); + + test('pressing a broken-URL placeholder does emit onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

Before after.

` + ); + + await expect(page.locator(`${sel.displayInner} img`)).toBeVisible({ + timeout: VISIBILITY_TIMEOUT_MS, + }); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: BROKEN_IMAGE_SRC, width: 40, height: 40 } }); + }); +}); + +test.describe('EnrichedText press inside lists', () => { + const listCases = [ + { + name: 'unordered list', + open: '
    ', + close: '
', + }, + { + name: 'ordered list', + open: '
    ', + close: '
', + }, + // { + // name: 'checkbox list', + // open: '
    ', + // close: '
', + // }, + ]; + + for (const c of listCases) { + test(`pressing an image inside a ${c.name} emits onImagePress`, async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `${c.open}
  • See tiny

  • Other item

  • ${c.close}` + ); + + await expect(imagePress(page)).toHaveText('null'); + + await page.locator(`${sel.displayInner} li img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: IMAGE_SRC, width: 28, height: 28 } }); + }); + } +}); diff --git a/apps/example-web/src/testScreens/TestEnrichedText.tsx b/apps/example-web/src/testScreens/TestEnrichedText.tsx index 912031dca..39f4a1d22 100644 --- a/apps/example-web/src/testScreens/TestEnrichedText.tsx +++ b/apps/example-web/src/testScreens/TestEnrichedText.tsx @@ -1,5 +1,8 @@ import { useState, type ChangeEvent } from 'react'; -import { EnrichedText } from 'react-native-enriched-html'; +import { + EnrichedText, + type OnImagePressEvent, +} from 'react-native-enriched-html'; import type { TextStyle } from 'react-native'; import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle'; @@ -8,6 +11,8 @@ const INITIAL_VALUE = '

    '; export function TestEnrichedText() { const [htmlInput, setHtmlInput] = useState(INITIAL_VALUE); const [value, setValue] = useState(INITIAL_VALUE); + const [lastImagePress, setLastImagePress] = + useState(null); return (
    @@ -18,6 +23,7 @@ export function TestEnrichedText() { {value} @@ -42,6 +48,10 @@ export function TestEnrichedText() {
    {value}
    + +
    +        {JSON.stringify(lastImagePress)}
    +      
    ); } From daacfb8eeb7c4c60e54b0d030ad216d291475689 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Fri, 10 Jul 2026 12:00:52 +0200 Subject: [PATCH 3/9] docs: update --- docs/TEXT_API_REFERENCE.md | 6 +++--- docs/WEB.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/TEXT_API_REFERENCE.md b/docs/TEXT_API_REFERENCE.md index b9d6320e8..9c0165254 100644 --- a/docs/TEXT_API_REFERENCE.md +++ b/docs/TEXT_API_REFERENCE.md @@ -128,9 +128,9 @@ interface OnImagePressEvent { } ``` -| Type | Default Value | Platform | -| ------------------------------------ | ------------- | ------------ | -| `(event: OnImagePressEvent) => void` | - | iOS, Android | +| Type | Default Value | Platform | +| ------------------------------------ | ------------- | ----------------- | +| `(event: OnImagePressEvent) => void` | - | iOS, Android, Web | > [!NOTE] > No visual feedback is applied on press. diff --git a/docs/WEB.md b/docs/WEB.md index 34e5342a1..0af770f3f 100644 --- a/docs/WEB.md +++ b/docs/WEB.md @@ -39,6 +39,7 @@ See [Web Keyboard Shortcuts](./INPUT_API_REFERENCE.md#web-keyboard-shortcuts) fo ### What works - Customizing the styling using props: `style`, `htmlStyle`, `selectionColor`. +- `onImagePress` callback ### Unsupported From 6767b26ba0f29f9d925194f5fbd5446d30321e09 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Fri, 10 Jul 2026 12:03:11 +0200 Subject: [PATCH 4/9] fix: checkbox pointer event --- .playwright/tests/enrichedTextPress.spec.ts | 10 +++++----- src/web/EnrichedText.css | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.playwright/tests/enrichedTextPress.spec.ts b/.playwright/tests/enrichedTextPress.spec.ts index 528631e35..af23349cc 100644 --- a/.playwright/tests/enrichedTextPress.spec.ts +++ b/.playwright/tests/enrichedTextPress.spec.ts @@ -160,11 +160,11 @@ test.describe('EnrichedText press inside lists', () => { open: '
      ', close: '
    ', }, - // { - // name: 'checkbox list', - // open: '
      ', - // close: '
    ', - // }, + { + name: 'checkbox list', + open: '
      ', + close: '
    ', + }, ]; for (const c of listCases) { diff --git a/src/web/EnrichedText.css b/src/web/EnrichedText.css index 0e8878b46..01f3a1889 100644 --- a/src/web/EnrichedText.css +++ b/src/web/EnrichedText.css @@ -271,7 +271,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)); } @@ -284,6 +283,7 @@ height: var(--et-checkbox-box-size, 24px); margin: 0 var(--et-checkbox-gap-width, 16px) 0 0; flex-shrink: 0; + pointer-events: none; accent-color: var(--et-checkbox-box-color, #0000ff); } @@ -293,7 +293,6 @@ gap: 0; margin: 0; padding: 0; - user-select: none; } .et-view img { From 1c7662031bce796147899bea9d4b743d2bd02515 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Mon, 13 Jul 2026 11:37:03 +0200 Subject: [PATCH 5/9] feat: cleanup --- src/web/usePressInteractions.ts | 10 +++++++--- src/web/usePressInteractions.tsx | 22 ---------------------- 2 files changed, 7 insertions(+), 25 deletions(-) delete mode 100644 src/web/usePressInteractions.tsx diff --git a/src/web/usePressInteractions.ts b/src/web/usePressInteractions.ts index 94fb550d5..cd6fd67a6 100644 --- a/src/web/usePressInteractions.ts +++ b/src/web/usePressInteractions.ts @@ -22,12 +22,16 @@ export function usePressInteractions( const handleInteraction = (e: MouseEvent) => { const target = e.target as HTMLElement; - const image = target.closest('img'); + const anchor = target.closest('a'); + if (anchor && container.contains(anchor)) { + e.preventDefault(); + } + const image = target.closest('img'); if (image && container.contains(image)) { e.preventDefault(); - const imageAttributes = parseImageAttributs(image); + const imageAttributes = parseImageAttributes(image); if (imageAttributes) { onImagePressRef.current?.({ @@ -42,7 +46,7 @@ export function usePressInteractions( }, [containerRef, onImagePressRef]); } -function parseImageAttributs(image: HTMLElement): ImageAttributes | undefined { +function parseImageAttributes(image: HTMLElement): ImageAttributes | undefined { const uri = image.getAttribute('src'); const rawWidth = image.getAttribute('width'); const rawHeight = image.getAttribute('height'); diff --git a/src/web/usePressInteractions.tsx b/src/web/usePressInteractions.tsx deleted file mode 100644 index 1b9c0e879..000000000 --- a/src/web/usePressInteractions.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; - -export function usePressInteractions( - containerRef: React.RefObject -) { - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - const handleClick = (e: MouseEvent) => { - const target = e.target as HTMLElement; - - const anchor = target.closest('a'); - if (anchor && container.contains(anchor)) { - e.preventDefault(); - } - }; - - container.addEventListener('click', handleClick); - return () => container.removeEventListener('click', handleClick); - }, [containerRef]); -} From eab9d31b1e2343575785f1f022c96b8baa20151d Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Mon, 13 Jul 2026 11:53:13 +0200 Subject: [PATCH 6/9] fix: onImagePress when no explicit width and height attrs present --- src/web/usePressInteractions.ts | 36 ++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/web/usePressInteractions.ts b/src/web/usePressInteractions.ts index cd6fd67a6..d8f97beb0 100644 --- a/src/web/usePressInteractions.ts +++ b/src/web/usePressInteractions.ts @@ -31,7 +31,7 @@ export function usePressInteractions( if (image && container.contains(image)) { e.preventDefault(); - const imageAttributes = parseImageAttributes(image); + const imageAttributes = getImageAttributes(image); if (imageAttributes) { onImagePressRef.current?.({ @@ -46,23 +46,41 @@ export function usePressInteractions( }, [containerRef, onImagePressRef]); } -function parseImageAttributes(image: HTMLElement): ImageAttributes | undefined { +function getImageAttributes( + image: HTMLImageElement +): ImageAttributes | undefined { const uri = image.getAttribute('src'); + + if (!uri) { + return undefined; + } + + const { width, height } = getImageDimensions(image); + + return { + uri, + width, + height, + }; +} + +function getImageDimensions(image: HTMLImageElement): { + width: number; + height: number; +} { const rawWidth = image.getAttribute('width'); const rawHeight = image.getAttribute('height'); - if (uri && rawWidth && rawHeight) { + if (rawWidth && rawHeight) { const width = parseInt(rawWidth, 10); const height = parseInt(rawHeight, 10); if (!isNaN(width) && !isNaN(height)) { - return { - uri, - width, - height, - }; + return { width, height }; } } - return undefined; + const rect = image.getBoundingClientRect(); + + return { width: rect.width, height: rect.height }; } From 388ad7d1a0e62645fefa1ef4a389b087543b75c9 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Tue, 21 Jul 2026 14:49:47 +0200 Subject: [PATCH 7/9] feat: temp merge --- .../{enrichedTextPress.spec.ts => enrichedTextPresstemp.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .playwright/tests/{enrichedTextPress.spec.ts => enrichedTextPresstemp.spec.ts} (100%) diff --git a/.playwright/tests/enrichedTextPress.spec.ts b/.playwright/tests/enrichedTextPresstemp.spec.ts similarity index 100% rename from .playwright/tests/enrichedTextPress.spec.ts rename to .playwright/tests/enrichedTextPresstemp.spec.ts From e0cc0e00b2eb48ed4cef10feba79877fba317b8b Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Tue, 21 Jul 2026 15:38:50 +0200 Subject: [PATCH 8/9] refactor: remove temporary merge file --- .playwright/tests/enrichedTextPress.spec.ts | 143 +++++++++++++ .../tests/enrichedTextPresstemp.spec.ts | 191 ------------------ 2 files changed, 143 insertions(+), 191 deletions(-) delete mode 100644 .playwright/tests/enrichedTextPresstemp.spec.ts diff --git a/.playwright/tests/enrichedTextPress.spec.ts b/.playwright/tests/enrichedTextPress.spec.ts index 466cd42da..079930022 100644 --- a/.playwright/tests/enrichedTextPress.spec.ts +++ b/.playwright/tests/enrichedTextPress.spec.ts @@ -2,6 +2,15 @@ import { test, expect, type Page } from '@playwright/test'; test.setTimeout(90_000); +const IMAGE_ROUTE = '**/pw-e2e-ok.png'; +const IMAGE_SRC = '/pw-e2e-ok.png'; +const BROKEN_IMAGE_ROUTE = '**/pw-e2e-broken.png'; +const BROKEN_IMAGE_SRC = '/pw-e2e-broken.png'; +const PNG_BODY = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64' +); + const sel = { root: '[data-testid="test-enriched-text-root"]', htmlInput: '[data-testid="test-enriched-text-html-input"]', @@ -11,8 +20,22 @@ const sel = { 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"]', + imagePressOutput: '[data-testid="test-enriched-text-image-press-output"]', } as const; +const VISIBILITY_TIMEOUT_MS = 1_000; + +test.beforeEach(async ({ page }) => { + await page.route(IMAGE_ROUTE, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'image/png', + body: PNG_BODY, + }); + }); + await page.route(BROKEN_IMAGE_ROUTE, (route) => route.abort()); +}); + async function gotoTestEnrichedText(page: Page): Promise { await page.goto('/test-enriched-text'); await page.waitForSelector(sel.displayInner); @@ -35,6 +58,10 @@ function mentionPress(page: Page) { return page.locator(sel.mentionPressOutput); } +function imagePress(page: Page) { + return page.locator(sel.imagePressOutput); +} + test.describe('EnrichedText link press', () => { test('pressing a link emits onLinkPress with the url', async ({ page }) => { await gotoTestEnrichedText(page); @@ -143,6 +170,102 @@ test.describe('EnrichedText mention press', () => { }); }); +test.describe('EnrichedText image press', () => { + test('pressing an image emits onImagePress with the image', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

    Look now.

    ` + ); + + await expect(imagePress(page)).toHaveText('null'); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: IMAGE_SRC, width: 32, height: 32 } }); + }); + + test('pressing an image surrounded by styled text still emits onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

    A bold here.

    ` + ); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: IMAGE_SRC, width: 120, height: 60 } }); + }); + + test('pressing non-image text does not emit onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

    Plain text with an .

    ` + ); + + await page + .locator(`${sel.displayInner} p`) + .click({ position: { x: 2, y: 2 } }); + + await expect(imagePress(page)).toHaveText('null'); + }); + + test('pressing an empty-src placeholder does not emit onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + '

    Before after.

    ' + ); + + await expect(page.locator(`${sel.displayInner} img`)).toBeVisible({ + timeout: VISIBILITY_TIMEOUT_MS, + }); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect(imagePress(page)).toHaveText('null'); + }); + + test('pressing a broken-URL placeholder does emit onImagePress', async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `

    Before after.

    ` + ); + + await expect(page.locator(`${sel.displayInner} img`)).toBeVisible({ + timeout: VISIBILITY_TIMEOUT_MS, + }); + + await page.locator(`${sel.displayInner} img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: BROKEN_IMAGE_SRC, width: 40, height: 40 } }); + }); +}); + test.describe('EnrichedText press inside lists', () => { const listCases = [ { @@ -206,5 +329,25 @@ test.describe('EnrichedText press inside lists', () => { attributes: { id: '1' }, }); }); + + test(`pressing an image inside a ${c.name} emits onImagePress`, async ({ + page, + }) => { + await gotoTestEnrichedText(page); + await setEnrichedTextValue( + page, + `${c.open}
  • See tiny

  • Other item

  • ${c.close}` + ); + + await expect(imagePress(page)).toHaveText('null'); + + await page.locator(`${sel.displayInner} li img`).click(); + + await expect + .poll(async () => + JSON.parse((await imagePress(page).textContent()) || 'null') + ) + .toEqual({ image: { uri: IMAGE_SRC, width: 28, height: 28 } }); + }); } }); diff --git a/.playwright/tests/enrichedTextPresstemp.spec.ts b/.playwright/tests/enrichedTextPresstemp.spec.ts deleted file mode 100644 index af23349cc..000000000 --- a/.playwright/tests/enrichedTextPresstemp.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.setTimeout(90_000); - -const IMAGE_ROUTE = '**/pw-e2e-ok.png'; -const IMAGE_SRC = '/pw-e2e-ok.png'; -const BROKEN_IMAGE_ROUTE = '**/pw-e2e-broken.png'; -const BROKEN_IMAGE_SRC = '/pw-e2e-broken.png'; -const PNG_BODY = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', - 'base64' -); - -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', - imagePressOutput: '[data-testid="test-enriched-text-image-press-output"]', -} as const; - -const VISIBILITY_TIMEOUT_MS = 1_000; - -test.beforeEach(async ({ page }) => { - await page.route(IMAGE_ROUTE, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'image/png', - body: PNG_BODY, - }); - }); - await page.route(BROKEN_IMAGE_ROUTE, (route) => route.abort()); -}); - -async function gotoTestEnrichedText(page: Page): Promise { - await page.goto('/test-enriched-text'); - await page.waitForSelector(sel.displayInner); -} - -async function setEnrichedTextValue(page: Page, html: string): Promise { - await page.fill(sel.htmlInput, html); - await page.click(sel.setValueButton); - - await expect - .poll(async () => (await page.locator(sel.valueOutput).textContent()) ?? '') - .toBe(html); -} - -function imagePress(page: Page) { - return page.locator(sel.imagePressOutput); -} - -test.describe('EnrichedText image press', () => { - test('pressing an image emits onImagePress with the image', async ({ - page, - }) => { - await gotoTestEnrichedText(page); - await setEnrichedTextValue( - page, - `

    Look now.

    ` - ); - - await expect(imagePress(page)).toHaveText('null'); - - await page.locator(`${sel.displayInner} img`).click(); - - await expect - .poll(async () => - JSON.parse((await imagePress(page).textContent()) || 'null') - ) - .toEqual({ image: { uri: IMAGE_SRC, width: 32, height: 32 } }); - }); - - test('pressing an image surrounded by styled text still emits onImagePress', async ({ - page, - }) => { - await gotoTestEnrichedText(page); - await setEnrichedTextValue( - page, - `

    A bold here.

    ` - ); - - await page.locator(`${sel.displayInner} img`).click(); - - await expect - .poll(async () => - JSON.parse((await imagePress(page).textContent()) || 'null') - ) - .toEqual({ image: { uri: IMAGE_SRC, width: 120, height: 60 } }); - }); - - test('pressing non-image text does not emit onImagePress', async ({ - page, - }) => { - await gotoTestEnrichedText(page); - await setEnrichedTextValue( - page, - `

    Plain text with an .

    ` - ); - - await page - .locator(`${sel.displayInner} p`) - .click({ position: { x: 2, y: 2 } }); - - await expect(imagePress(page)).toHaveText('null'); - }); - - test('pressing an empty-src placeholder does not emit onImagePress', async ({ - page, - }) => { - await gotoTestEnrichedText(page); - await setEnrichedTextValue( - page, - '

    Before after.

    ' - ); - - await expect(page.locator(`${sel.displayInner} img`)).toBeVisible({ - timeout: VISIBILITY_TIMEOUT_MS, - }); - - await page.locator(`${sel.displayInner} img`).click(); - - await expect(imagePress(page)).toHaveText('null'); - }); - - test('pressing a broken-URL placeholder does emit onImagePress', async ({ - page, - }) => { - await gotoTestEnrichedText(page); - await setEnrichedTextValue( - page, - `

    Before after.

    ` - ); - - await expect(page.locator(`${sel.displayInner} img`)).toBeVisible({ - timeout: VISIBILITY_TIMEOUT_MS, - }); - - await page.locator(`${sel.displayInner} img`).click(); - - await expect - .poll(async () => - JSON.parse((await imagePress(page).textContent()) || 'null') - ) - .toEqual({ image: { uri: BROKEN_IMAGE_SRC, width: 40, height: 40 } }); - }); -}); - -test.describe('EnrichedText press inside lists', () => { - const listCases = [ - { - name: 'unordered list', - open: '
      ', - close: '
    ', - }, - { - name: 'ordered list', - open: '
      ', - close: '
    ', - }, - { - name: 'checkbox list', - open: '
      ', - close: '
    ', - }, - ]; - - for (const c of listCases) { - test(`pressing an image inside a ${c.name} emits onImagePress`, async ({ - page, - }) => { - await gotoTestEnrichedText(page); - await setEnrichedTextValue( - page, - `${c.open}
  • See tiny

  • Other item

  • ${c.close}` - ); - - await expect(imagePress(page)).toHaveText('null'); - - await page.locator(`${sel.displayInner} li img`).click(); - - await expect - .poll(async () => - JSON.parse((await imagePress(page).textContent()) || 'null') - ) - .toEqual({ image: { uri: IMAGE_SRC, width: 28, height: 28 } }); - }); - } -}); From 94a9151599da4b80bf34bc666e9253d7eab6507c Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Wed, 22 Jul 2026 10:25:42 +0200 Subject: [PATCH 9/9] refactor: cleanup imports --- src/web/usePressInteractions.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/web/usePressInteractions.ts b/src/web/usePressInteractions.ts index 028ff6321..0856c5c16 100644 --- a/src/web/usePressInteractions.ts +++ b/src/web/usePressInteractions.ts @@ -1,5 +1,17 @@ import { useEffect, type RefObject } from 'react'; -import type { OnImagePressEvent } from '../types'; +import type { + OnLinkPressEvent, + OnMentionPressEvent, + OnImagePressEvent, +} from '../types'; + +type OnLinkPressEventRef = RefObject< + ((event: OnLinkPressEvent) => void) | undefined +>; + +type OnMentionPressEventRef = RefObject< + ((event: OnMentionPressEvent) => void) | undefined +>; type OnImagePressEventRef = RefObject< ((event: OnImagePressEvent) => void) | undefined @@ -11,16 +23,6 @@ type ImageAttributes = { height: number; }; -import type { OnLinkPressEvent, OnMentionPressEvent } from '../types'; - -type OnLinkPressEventRef = RefObject< - ((event: OnLinkPressEvent) => void) | undefined ->; - -type OnMentionPressEventRef = RefObject< - ((event: OnMentionPressEvent) => void) | undefined ->; - export function usePressInteractions( containerRef: RefObject, onLinkPressRef: OnLinkPressEventRef,