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); }