diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx
index 8fa6e035a..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,15 +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/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/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/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 c26efb589..40c750bbb 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';
@@ -7,23 +13,47 @@ 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';
import { INLINE_IMAGE_CSS_VARIABLES } from './styleConversion/inlineImageCSSVariables';
import { useImageErrorFallback } from './useImageErrorFallback';
import { usePressInteractions } from './usePressInteractions';
+import { adaptWebToNativeEvent } from './adaptWebToNativeEvent';
export const EnrichedText = memo(
- ({ children, htmlStyle, style, selectionColor }: EnrichedTextProps) => {
+ ({
+ ref,
+ children,
+ htmlStyle,
+ style,
+ selectionColor,
+ selectable = false,
+ useHtmlNormalizer = false,
+ onFocus,
+ onBlur,
+ }: 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(
- () => prepareHtmlForWeb(sanitizedHtml),
- [sanitizedHtml]
+ () => prepareHtmlForWeb(sanitizedHtml, useHtmlNormalizer),
+ [sanitizedHtml, useHtmlNormalizer]
);
const resolvedHtmlStyle = useMemo(
@@ -45,8 +75,8 @@ export const EnrichedText = memo(
);
const themingStyle = useMemo(
- () => enrichedInputThemingToCSSProperties({ selectionColor }),
- [selectionColor]
+ () => enrichedTextThemingToCSSProperties({ selectionColor, selectable }),
+ [selectionColor, selectable]
);
const mentionRulesCSS = useMemo(
@@ -72,8 +102,15 @@ export const EnrichedText = memo(
{mentionRulesCSS ? : null}
+ onFocus?.(adaptWebToNativeEvent(event, { target: -1 }))
+ }
+ onBlur={(event) =>
+ onBlur?.(adaptWebToNativeEvent(event, { target: -1 }))
+ }
dangerouslySetInnerHTML={{ __html: finalHtml }}
/>
>
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);
}