Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 3 additions & 21 deletions apps/example-web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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: '',
Expand Down Expand Up @@ -332,15 +332,7 @@ function App() {

{showHtmlOutput && <HtmlOutputPanel html={currentHtml} />}

<div className="container enriched-text-container">
<h1 className="app-title">Enriched Text</h1>
<EnrichedText
style={enrichedTextStyle}
htmlStyle={WEB_DEFAULT_HTML_STYLE}
>
{enrichedTextValue}
</EnrichedText>
</div>
<TextRenderer htmlValue={enrichedTextValue} />

{isSetValueModalOpen && (
<SetValueModal
Expand Down Expand Up @@ -382,14 +374,4 @@ const enrichedInputStyle: EnrichedInputStyle = {
fontSize: 18,
};

const enrichedTextStyle: TextStyle = {
backgroundColor: 'gainsboro',
width: '100%',
marginVertical: 12,
paddingVertical: 12,
paddingHorizontal: 14,
borderRadius: 8,
fontSize: 18,
};

export default App;
2 changes: 1 addition & 1 deletion apps/example-web/src/components/EditorActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import './EditorActions.css';
import './Actions.css';

interface EditorActionsProps {
showHtmlOutput: boolean;
Expand Down
19 changes: 19 additions & 0 deletions apps/example-web/src/components/TextActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import './Actions.css';

interface TextActionsProps {
onFocus: () => void;
onBlur: () => void;
}

export function TextActions({ onFocus, onBlur }: TextActionsProps) {
return (
<div className="btn-row" data-testid="text-actions-row">
<button className="btn" data-testid="text-focus-button" onClick={onFocus}>
Focus
</button>
<button className="btn" data-testid="text-blur-button" onClick={onBlur}>
Blur
</button>
</div>
);
}
67 changes: 67 additions & 0 deletions apps/example-web/src/components/TextRenderer.tsx
Original file line number Diff line number Diff line change
@@ -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<EnrichedTextInstance>(null);

const handleTextFocus = (e: FocusEvent) => {
console.log('[EnrichedText] onFocus', e.nativeEvent);
};

const handleTextBlur = (e: BlurEvent) => {
console.log('[EnrichedText] onBlur', e.nativeEvent);
};

return (
<div className="container enriched-text-container">
<h1 className="app-title">Enriched Text</h1>
<EnrichedText
ref={ref}
style={enrichedTextStyle}
htmlStyle={WEB_DEFAULT_HTML_STYLE}
useHtmlNormalizer
onFocus={handleTextFocus}
onBlur={handleTextBlur}
>
{htmlValue}
</EnrichedText>
<div className="btn-row" data-testid="editor-actions-row">
<button
className="btn"
data-testid="focus-button"
onClick={() => ref.current?.focus()}
>
Focus
</button>
<button
className="btn"
data-testid="blur-button"
onClick={() => ref.current?.blur()}
>
Blur
</button>
</div>
</div>
);
}

const enrichedTextStyle: TextStyle = {
backgroundColor: 'gainsboro',
width: '100%',
marginVertical: 12,
paddingVertical: 12,
paddingHorizontal: 14,
borderRadius: 8,
fontSize: 18,
};
12 changes: 6 additions & 6 deletions docs/TEXT_API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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`

Expand Down
5 changes: 3 additions & 2 deletions docs/WEB.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type {
FocusEvent,
BlurEvent,
EnrichedTextInputInstance,
EnrichedTextInstance,
ContextMenuItem,
OnChangeMentionEvent,
EnrichedTextHtmlStyle,
Expand Down
51 changes: 44 additions & 7 deletions src/web/EnrichedText.tsx
Original file line number Diff line number Diff line change
@@ -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';
Comment thread
hejsztynx marked this conversation as resolved.
import { enrichedTextStyleToCSSProperties } from './styleConversion/enrichedTextStyleToCSSProperties';
Expand All @@ -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,
Comment thread
hejsztynx marked this conversation as resolved.
style,
selectionColor,
selectable = false,
useHtmlNormalizer = false,
onFocus,
onBlur,
}: EnrichedTextProps) => {
const containerRef = useRef<HTMLDivElement>(null);

useImperativeHandle(ref, () => ({
measureInWindow: () => {},
measure: () => {},
measureLayout: () => {},
setNativeProps: () => {},
Comment thread
szydlovsky marked this conversation as resolved.
focus: () => {
containerRef.current?.focus();
},
blur: () => {
containerRef.current?.blur();
},
Comment thread
szydlovsky marked this conversation as resolved.
}));

const sanitizedHtml = useMemo(() => sanitizeHtml(children), [children]);

const finalHtml = useMemo(
() => prepareHtmlForWeb(sanitizedHtml),
[sanitizedHtml]
() => prepareHtmlForWeb(sanitizedHtml, useHtmlNormalizer),
[sanitizedHtml, useHtmlNormalizer]
);

const resolvedHtmlStyle = useMemo(
Expand All @@ -45,8 +75,8 @@ export const EnrichedText = memo(
);

const themingStyle = useMemo(
() => enrichedInputThemingToCSSProperties({ selectionColor }),
[selectionColor]
() => enrichedTextThemingToCSSProperties({ selectionColor, selectable }),
[selectionColor, selectable]
);

const mentionRulesCSS = useMemo(
Expand All @@ -72,8 +102,15 @@ export const EnrichedText = memo(
{mentionRulesCSS ? <style>{mentionRulesCSS}</style> : null}
<div
ref={containerRef}
tabIndex={-1}
Comment thread
hejsztynx marked this conversation as resolved.
style={finalStyle}
className={ENRICHED_TEXT_CLASSNAME}
onFocus={(event) =>
onFocus?.(adaptWebToNativeEvent(event, { target: -1 }))
}
onBlur={(event) =>
onBlur?.(adaptWebToNativeEvent(event, { target: -1 }))
}
dangerouslySetInnerHTML={{ __html: finalHtml }}
/>
</>
Expand Down
10 changes: 9 additions & 1 deletion src/web/normalization/prepareHtmlForWeb.ts
Original file line number Diff line number Diff line change
@@ -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);
}

Expand Down
34 changes: 28 additions & 6 deletions src/web/styleConversion/enrichedThemingToCSSProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import type { CSSProperties } from 'react';
import type { ColorValue } from 'react-native';
import { toColor } from './toColor';

type EnrichedInputThemingStyle = Partial<
Pick<CSSProperties, 'caretColor'> & {
type EnrichedThemingStyle = Partial<
Pick<CSSProperties, 'caretColor' | 'userSelect'> & {
'--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;
Expand All @@ -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';
Comment thread
kacperzolkiewski marked this conversation as resolved.

return applySelectionColor(extra, selectionColor);
}
Loading