Skip to content

Commit 6723ce9

Browse files
authored
feat(web): HTML normalization (#630)
# Summary Web implementation was missing the HTML normalizer - implemented the normalizer, mirroring the one in the native code - tests also mirror the native ones ## Test Plan Try pasting styled text from `Google Docs`, `MS Word` or any other platform ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ❌ | | Web | ✅ | ## Checklist - [x] E2E tests are passing - [ ] Required E2E tests have been added (if applicable)
1 parent 4953d40 commit 6723ce9

11 files changed

Lines changed: 1547 additions & 22 deletions

File tree

apps/example-web/src/App.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ function App() {
261261
onMentionDetected={handleOnMentionDetected}
262262
mentionIndicators={['@', '#']}
263263
htmlStyle={WEB_DEFAULT_HTML_STYLE}
264+
useHtmlNormalizer
264265
/>
265266
<MentionPopup
266267
variant="user"

docs/INPUT_API_REFERENCE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -643,9 +643,9 @@ If true, Android will use experimental synchronous events. This will prevent fro
643643

644644
If true, external HTML pasted/inserted into the input (e.g. from Google Docs, Word, or web pages) will be normalized into the canonical tag subset that the enriched parser understands. However, this is an experimental feature, which has not been thoroughly tested. We may decide to enable it by default in a future release.
645645

646-
| Type | Default Value | Platform |
647-
| ------ | ------------- | ------------ |
648-
| `bool` | `false` | iOS, Android |
646+
| Type | Default Value | Platform |
647+
| ------ | ------------- | ----------------- |
648+
| `bool` | `false` | iOS, Android, Web |
649649

650650
## Ref Methods
651651

docs/WEB.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Web support is still experimental. APIs and behavior can change in future releas
1616
- Submit props: `submitBehavior` and `onSubmitEditing`. `returnKeyType` is only a hint, it maps to [enterkeyhint](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint) (`done`, `go`, `next`, `previous`, `search`, `send`, `default`/`enter`). Not all values of `ReturnKeyTypeOptions` are supported, the behavior of this prop is heavily dependent on the browser's capabilities.
1717
- Input theming via `placeholderTextColor`, `cursorColor` and `selectionColor` props
1818
- Keyboard shortcuts for formatting
19+
- `useHtmlNormalizer`
1920

2021
## Keyboard shortcuts
2122

@@ -26,7 +27,6 @@ See [Web Keyboard Shortcuts](./INPUT_API_REFERENCE.md#web-keyboard-shortcuts) fo
2627
- **`returnKeyLabel`**: ignored on web, it's not possible to set it inside a browser.
2728
- **Automatic link detection**: `linkRegex` is ignored. Links only work when set explicitly via the `setLink` ref method.
2829
- **Context menu**: `contextMenuItems` is ignored.
29-
- **HTML normalizer flag**: `useHtmlNormalizer` is ignored; paste behavior follows the browser pipeline.
3030
- **RN layout ref methods**: `measure`, `measureInWindow`, `measureLayout`, and `setNativeProps` are no-ops.
3131
- **`EnrichedText`**: The read-only component is not exported on web.
3232
- **`ViewProps`**: Props inherited from `View` beyond the implemented subset are not forwarded.

package.json

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"eslint-config-prettier": "^10.1.1",
128128
"eslint-plugin-prettier": "^5.2.3",
129129
"jest": "^29.7.0",
130+
"jest-environment-jsdom": "^29.7.0",
130131
"playwright": "1.58.2",
131132
"prettier": "^3.0.3",
132133
"react": "19.1.0",
@@ -147,15 +148,35 @@
147148
],
148149
"packageManager": "yarn@4.13.0",
149150
"jest": {
150-
"preset": "react-native",
151-
"modulePathIgnorePatterns": [
152-
"<rootDir>/apps/example/node_modules",
153-
"<rootDir>/lib/"
154-
],
155-
"testPathIgnorePatterns": [
156-
"/node_modules/",
157-
"<rootDir>/.playwright/",
158-
"<rootDir>/apps/example-web/"
151+
"projects": [
152+
{
153+
"displayName": "native",
154+
"preset": "react-native",
155+
"modulePathIgnorePatterns": [
156+
"<rootDir>/apps/example/node_modules",
157+
"<rootDir>/lib/"
158+
],
159+
"testPathIgnorePatterns": [
160+
"/node_modules/",
161+
"<rootDir>/.playwright/",
162+
"<rootDir>/apps/example-web/",
163+
"<rootDir>/src/web/"
164+
]
165+
},
166+
{
167+
"displayName": "web",
168+
"testEnvironment": "jsdom",
169+
"testMatch": [
170+
"<rootDir>/src/web/**/__tests__/**/*.test.{ts,tsx}"
171+
],
172+
"modulePathIgnorePatterns": [
173+
"<rootDir>/apps/example/node_modules",
174+
"<rootDir>/lib/"
175+
],
176+
"transform": {
177+
"^.+\\.(js|ts|tsx)$": "babel-jest"
178+
}
179+
}
159180
]
160181
},
161182
"commitlint": {

src/web/EnrichedTextInput.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { useOnLinkDetected } from './useOnLinkDetected';
3535
import {
3636
prepareHtmlForTiptap,
3737
normalizeHtmlFromTiptap,
38-
} from './tiptapHtmlNormalizer';
38+
} from './normalization/tiptapHtmlNormalizer';
3939
import { ENRICHED_TEXT_INPUT_DEFAULT_PROPS } from '../utils/EnrichedTextInputDefaultProps';
4040
import { enrichedInputStyleToCSSProperties } from './styleConversion/enrichedInputStyleToCSSProperties';
4141
import { enrichedInputThemingToCSSProperties } from './styleConversion/enrichedInputThemingToCSSProperties';
@@ -111,9 +111,12 @@ export const EnrichedTextInput = ({
111111
onChangeMention,
112112
onEndMention,
113113
htmlStyle,
114+
useHtmlNormalizer,
114115
}: EnrichedTextInputProps) => {
115116
const tiptapContent =
116-
defaultValue != null ? prepareHtmlForTiptap(defaultValue) : defaultValue;
117+
defaultValue != null
118+
? prepareHtmlForTiptap(defaultValue, useHtmlNormalizer)
119+
: defaultValue;
117120

118121
const resolvedHtmlStyle = useMemo(
119122
() => mergeWithDefaultHtmlStyle(htmlStyle),
@@ -165,6 +168,11 @@ export const EnrichedTextInput = ({
165168
onKeyPressRef.current = onKeyPress;
166169
}, [onKeyPress]);
167170

171+
const useHtmlNormalizerRef = useRef(useHtmlNormalizer);
172+
useEffect(() => {
173+
useHtmlNormalizerRef.current = useHtmlNormalizer;
174+
}, [useHtmlNormalizer]);
175+
168176
const handleKeyDown = (doc: Node, event: KeyboardEvent): boolean => {
169177
onKeyPressRef.current?.(adaptWebToNativeEvent(event, { key: event.key }));
170178
if (event.key !== 'Enter') {
@@ -264,6 +272,9 @@ export const EnrichedTextInput = ({
264272
autoCapitalize,
265273
enterkeyhint: returnKeyTypeToEnterKeyHint(returnKeyType),
266274
},
275+
transformPastedHTML: (html) => {
276+
return prepareHtmlForTiptap(html, useHtmlNormalizerRef.current);
277+
},
267278
},
268279
},
269280
[tiptapContent, extensions]
@@ -307,7 +318,9 @@ export const EnrichedTextInput = ({
307318
focus: () => editor.commands.focus(),
308319
blur: () => editor.commands.blur(),
309320
setValue: (value: string) =>
310-
editor.commands.setContent(prepareHtmlForTiptap(value)),
321+
editor.commands.setContent(
322+
prepareHtmlForTiptap(value, useHtmlNormalizerRef.current)
323+
),
311324
setSelection: (start, end) => {
312325
const doc = editor.state.doc;
313326
runFocused(editor, (c) =>

0 commit comments

Comments
 (0)