From f7933e9039c5367490bbfb822fc69976dfddbd66 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Wed, 24 Jun 2026 14:16:27 +0200 Subject: [PATCH 1/4] feat: useHtmlNormalizer and selectable props --- apps/example-web/src/App.tsx | 1 + docs/TEXT_API_REFERENCE.md | 12 +++---- src/web/EnrichedText.tsx | 19 +++++++---- src/web/normalization/prepareHtmlForWeb.ts | 10 +++++- .../enrichedThemingToCSSProperties.ts | 34 +++++++++++++++---- 5 files changed, 57 insertions(+), 19 deletions(-) diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index 8fa6e035a..b7a3ae76f 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -337,6 +337,7 @@ function App() { {enrichedTextValue} diff --git a/docs/TEXT_API_REFERENCE.md b/docs/TEXT_API_REFERENCE.md index 145ddd237..938641da0 100644 --- a/docs/TEXT_API_REFERENCE.md +++ b/docs/TEXT_API_REFERENCE.md @@ -40,9 +40,9 @@ A prop for customizing styles of HTML elements, including press colors for inter If `true`, external HTML (e.g. from Google Docs, Word, web pages) will be normalized before rendering. This converts arbitrary HTML into the canonical tag subset that the enriched parser understands. -| Type | Default Value | Platform | -| ------ | ------------- | ------------ | -| `bool` | `false` | iOS, Android | +| Type | Default Value | Platform | +| ------ | ------------- | ----------------- | +| `bool` | `false` | iOS, Android, Web | ### `ellipsizeMode` @@ -72,9 +72,9 @@ Limits the number of displayed lines. Set to `0` for unlimited lines. If `true`, the text can be selected by the user (e.g. for copy/paste). -| Type | Default Value | Platform | -| ------ | ------------- | ------------ | -| `bool` | `false` | iOS, Android | +| Type | Default Value | Platform | +| ------ | ------------- | ----------------- | +| `bool` | `false` | iOS, Android, Web | ### `selectionColor` diff --git a/src/web/EnrichedText.tsx b/src/web/EnrichedText.tsx index c26efb589..d915dc7c0 100644 --- a/src/web/EnrichedText.tsx +++ b/src/web/EnrichedText.tsx @@ -7,7 +7,7 @@ import { mergeWithDefaultEnrichedTextHtmlStyle, } from './styleConversion/htmlStyleToCSSVariables'; import { ENRICHED_TEXT_CLASSNAME } from './constants/classNames'; -import { enrichedInputThemingToCSSProperties } from './styleConversion/enrichedThemingToCSSProperties'; +import { enrichedTextThemingToCSSProperties } from './styleConversion/enrichedThemingToCSSProperties'; import { buildMentionRulesCSS } from './styleConversion/buildMentionRulesCSS'; import { sanitizeHtml } from './sanitization/htmlSanitizer'; import { prepareHtmlForWeb } from './normalization/prepareHtmlForWeb'; @@ -16,14 +16,21 @@ import { useImageErrorFallback } from './useImageErrorFallback'; import { usePressInteractions } from './usePressInteractions'; export const EnrichedText = memo( - ({ children, htmlStyle, style, selectionColor }: EnrichedTextProps) => { + ({ + children, + htmlStyle, + style, + selectionColor, + selectable = false, + useHtmlNormalizer = false, + }: EnrichedTextProps) => { const containerRef = useRef(null); const sanitizedHtml = useMemo(() => sanitizeHtml(children), [children]); const finalHtml = useMemo( - () => prepareHtmlForWeb(sanitizedHtml), - [sanitizedHtml] + () => prepareHtmlForWeb(sanitizedHtml, useHtmlNormalizer), + [sanitizedHtml, useHtmlNormalizer] ); const resolvedHtmlStyle = useMemo( @@ -45,8 +52,8 @@ export const EnrichedText = memo( ); const themingStyle = useMemo( - () => enrichedInputThemingToCSSProperties({ selectionColor }), - [selectionColor] + () => enrichedTextThemingToCSSProperties({ selectionColor, selectable }), + [selectionColor, selectable] ); const mentionRulesCSS = useMemo( diff --git a/src/web/normalization/prepareHtmlForWeb.ts b/src/web/normalization/prepareHtmlForWeb.ts index fc2151476..1fa2566e8 100644 --- a/src/web/normalization/prepareHtmlForWeb.ts +++ b/src/web/normalization/prepareHtmlForWeb.ts @@ -1,4 +1,12 @@ -export function prepareHtmlForWeb(html: string) { +import { normalizeHtml } from './htmlNormalizer'; + +export function prepareHtmlForWeb( + html: string, + useHtmlNormalizer: boolean | undefined +) { + if (useHtmlNormalizer) { + html = normalizeHtml(html); + } return checkboxHtmlToWeb(html); } diff --git a/src/web/styleConversion/enrichedThemingToCSSProperties.ts b/src/web/styleConversion/enrichedThemingToCSSProperties.ts index d4ca7bb75..494d9a61f 100644 --- a/src/web/styleConversion/enrichedThemingToCSSProperties.ts +++ b/src/web/styleConversion/enrichedThemingToCSSProperties.ts @@ -2,13 +2,22 @@ import type { CSSProperties } from 'react'; import type { ColorValue } from 'react-native'; import { toColor } from './toColor'; -type EnrichedInputThemingStyle = Partial< - Pick & { +type EnrichedThemingStyle = Partial< + Pick & { '--et-placeholder-text-color': string; '--et-selection-color': string; } >; +function applySelectionColor( + extra: EnrichedThemingStyle, + selectionColor: ColorValue | undefined +): EnrichedThemingStyle { + const selectionCss = toColor(selectionColor); + if (selectionCss) extra['--et-selection-color'] = selectionCss; + return extra; +} + export interface EnrichedInputThemingColors { cursorColor?: ColorValue; placeholderTextColor?: ColorValue; @@ -20,15 +29,28 @@ export function enrichedInputThemingToCSSProperties({ placeholderTextColor, selectionColor, }: EnrichedInputThemingColors): CSSProperties { - const extra: EnrichedInputThemingStyle = {}; + const extra: EnrichedThemingStyle = {}; const caret = toColor(cursorColor); if (caret) extra.caretColor = caret; const placeholderCss = toColor(placeholderTextColor); if (placeholderCss) extra['--et-placeholder-text-color'] = placeholderCss; - const selectionCss = toColor(selectionColor); - if (selectionCss) extra['--et-selection-color'] = selectionCss; + return applySelectionColor(extra, selectionColor); +} - return extra; +export interface EnrichedTextThemingOptions { + selectionColor?: ColorValue; + selectable?: boolean; +} + +export function enrichedTextThemingToCSSProperties({ + selectionColor, + selectable, +}: EnrichedTextThemingOptions): CSSProperties { + const extra: EnrichedThemingStyle = {}; + + if (selectable !== undefined) extra.userSelect = selectable ? 'text' : 'none'; + + return applySelectionColor(extra, selectionColor); } From 78f3b8277b43da950e6add88faec9121f1f5ee30 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Mon, 6 Jul 2026 11:38:32 +0200 Subject: [PATCH 2/4] feat: ref methods --- docs/WEB.md | 5 +++-- src/web/EnrichedText.tsx | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/WEB.md b/docs/WEB.md index 0d729a6df..d0a04e801 100644 --- a/docs/WEB.md +++ b/docs/WEB.md @@ -38,14 +38,15 @@ See [Web Keyboard Shortcuts](./INPUT_API_REFERENCE.md#web-keyboard-shortcuts) fo ### What works - Customizing the styling using props: `style`, `htmlStyle`, `selectionColor`. +- `selectable` prop +- `useHtmlNormalizer` ### Unsupported -- **`selectable`**: ignored on web. -- **`useHtmlNormalizer`**: ignored on web. - **`ellipsizeMode`**: ignored on web. - **`numberOfLines`**: ignored on web. - **Press events**: `onLinkPress` and `onMentionPress` callbacks are ignored on web. +- **RN layout ref methods**: `measure`, `measureInWindow`, `measureLayout`, and `setNativeProps` are no-ops. ## HTML sanitization diff --git a/src/web/EnrichedText.tsx b/src/web/EnrichedText.tsx index d915dc7c0..c227f236c 100644 --- a/src/web/EnrichedText.tsx +++ b/src/web/EnrichedText.tsx @@ -1,4 +1,10 @@ -import { memo, useMemo, useRef, type CSSProperties } from 'react'; +import { + memo, + useImperativeHandle, + useMemo, + useRef, + type CSSProperties, +} from 'react'; import type { EnrichedTextProps } from '../types'; import './EnrichedText.css'; import { enrichedTextStyleToCSSProperties } from './styleConversion/enrichedTextStyleToCSSProperties'; @@ -17,6 +23,7 @@ import { usePressInteractions } from './usePressInteractions'; export const EnrichedText = memo( ({ + ref, children, htmlStyle, style, @@ -26,6 +33,19 @@ export const EnrichedText = memo( }: EnrichedTextProps) => { const containerRef = useRef(null); + useImperativeHandle(ref, () => ({ + measureInWindow: () => {}, + measure: () => {}, + measureLayout: () => {}, + setNativeProps: () => {}, + focus: () => { + containerRef.current?.focus(); + }, + blur: () => { + containerRef.current?.blur(); + }, + })); + const sanitizedHtml = useMemo(() => sanitizeHtml(children), [children]); const finalHtml = useMemo( @@ -79,6 +99,7 @@ export const EnrichedText = memo( {mentionRulesCSS ? : null}
Date: Mon, 6 Jul 2026 13:02:30 +0200 Subject: [PATCH 3/4] feat: onFocus onBlur --- apps/example-web/src/App.tsx | 25 +------ .../{EditorActions.css => Actions.css} | 0 .../src/components/EditorActions.tsx | 2 +- .../src/components/TextActions.tsx | 19 ++++++ .../src/components/TextRenderer.tsx | 67 +++++++++++++++++++ src/index.tsx | 1 + src/web/EnrichedText.tsx | 13 ++++ 7 files changed, 104 insertions(+), 23 deletions(-) rename apps/example-web/src/components/{EditorActions.css => Actions.css} (100%) create mode 100644 apps/example-web/src/components/TextActions.tsx create mode 100644 apps/example-web/src/components/TextRenderer.tsx diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index b7a3ae76f..07cad4e33 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -14,10 +14,9 @@ import { type OnSubmitEditing, type OnChangeMentionEvent, type OnMentionDetected, - EnrichedText, } from 'react-native-enriched-html'; import { WEB_DEFAULT_HTML_STYLE } from './defaultHtmlStyle'; -import type { NativeSyntheticEvent, TextStyle } from 'react-native'; +import type { NativeSyntheticEvent } from 'react-native'; import { EditorActions } from './components/EditorActions'; import { SetValueModal } from './components/SetValueModal'; import { ImageModal } from './components/ImageModal'; @@ -28,6 +27,7 @@ import { Toolbar } from './components/Toolbar'; import { MentionPopup, type MentionItem } from './components/MentionPopup'; import { useUserMention } from './hooks/useUserMention'; import { useChannelMention } from './hooks/useChannelMention'; +import { TextRenderer } from './components/TextRenderer'; const DEFAULT_LINK_STATE: OnLinkDetected = { text: '', @@ -332,16 +332,7 @@ function App() { {showHtmlOutput && } -
-

Enriched Text

- - {enrichedTextValue} - -
+ {isSetValueModalOpen && ( void; + onBlur: () => void; +} + +export function TextActions({ onFocus, onBlur }: TextActionsProps) { + return ( +
+ + +
+ ); +} diff --git a/apps/example-web/src/components/TextRenderer.tsx b/apps/example-web/src/components/TextRenderer.tsx new file mode 100644 index 000000000..ae1bfa326 --- /dev/null +++ b/apps/example-web/src/components/TextRenderer.tsx @@ -0,0 +1,67 @@ +import { useRef } from 'react'; +import type { TextStyle } from 'react-native'; +import { + EnrichedText, + type BlurEvent, + type EnrichedTextInstance, + type FocusEvent, +} from 'react-native-enriched-html'; +import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle'; + +interface TextRendererProps { + htmlValue: string; +} + +export function TextRenderer({ htmlValue }: TextRendererProps) { + const ref = useRef(null); + + const handleTextFocus = (e: FocusEvent) => { + console.log('[EnrichedText] onFocus', e.nativeEvent); + }; + + const handleTextBlur = (e: BlurEvent) => { + console.log('[EnrichedText] onBlur', e.nativeEvent); + }; + + return ( +
+

Enriched Text

+ + {htmlValue} + +
+ + +
+
+ ); +} + +const enrichedTextStyle: TextStyle = { + backgroundColor: 'gainsboro', + width: '100%', + marginVertical: 12, + paddingVertical: 12, + paddingHorizontal: 14, + borderRadius: 8, + fontSize: 18, +}; diff --git a/src/index.tsx b/src/index.tsx index 59d9054fa..02d34b743 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,6 +17,7 @@ export type { FocusEvent, BlurEvent, EnrichedTextInputInstance, + EnrichedTextInstance, ContextMenuItem, OnChangeMentionEvent, EnrichedTextHtmlStyle, diff --git a/src/web/EnrichedText.tsx b/src/web/EnrichedText.tsx index c227f236c..0cc316435 100644 --- a/src/web/EnrichedText.tsx +++ b/src/web/EnrichedText.tsx @@ -20,6 +20,7 @@ import { prepareHtmlForWeb } from './normalization/prepareHtmlForWeb'; import { INLINE_IMAGE_CSS_VARIABLES } from './styleConversion/inlineImageCSSVariables'; import { useImageErrorFallback } from './useImageErrorFallback'; import { usePressInteractions } from './usePressInteractions'; +import { adaptWebToNativeEvent } from './adaptWebToNativeEvent'; export const EnrichedText = memo( ({ @@ -30,6 +31,8 @@ export const EnrichedText = memo( selectionColor, selectable = false, useHtmlNormalizer = false, + onFocus, + onBlur, }: EnrichedTextProps) => { const containerRef = useRef(null); @@ -102,6 +105,16 @@ export const EnrichedText = memo( tabIndex={-1} style={finalStyle} className={ENRICHED_TEXT_CLASSNAME} + onFocus={ + onFocus + ? (event) => onFocus(adaptWebToNativeEvent(event, { target: -1 })) + : undefined + } + onBlur={ + onBlur + ? (event) => onBlur(adaptWebToNativeEvent(event, { target: -1 })) + : undefined + } dangerouslySetInnerHTML={{ __html: finalHtml }} /> From ca1998fd6b35b9cfb61480e662417a3c67e332b8 Mon Sep 17 00:00:00 2001 From: Krystian Sienkiewicz Date: Tue, 7 Jul 2026 20:57:18 +0200 Subject: [PATCH 4/4] refactor: simplified onFocus onBlur assignments --- src/web/EnrichedText.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/web/EnrichedText.tsx b/src/web/EnrichedText.tsx index 0cc316435..40c750bbb 100644 --- a/src/web/EnrichedText.tsx +++ b/src/web/EnrichedText.tsx @@ -105,15 +105,11 @@ export const EnrichedText = memo( tabIndex={-1} style={finalStyle} className={ENRICHED_TEXT_CLASSNAME} - onFocus={ - onFocus - ? (event) => onFocus(adaptWebToNativeEvent(event, { target: -1 })) - : undefined + onFocus={(event) => + onFocus?.(adaptWebToNativeEvent(event, { target: -1 })) } - onBlur={ - onBlur - ? (event) => onBlur(adaptWebToNativeEvent(event, { target: -1 })) - : undefined + onBlur={(event) => + onBlur?.(adaptWebToNativeEvent(event, { target: -1 })) } dangerouslySetInnerHTML={{ __html: finalHtml }} />