docs/solutions/ui-bugs/2026-05-07-slate-react-ime-formatted-selection-needs-native-owned-cleanup.md
The Lexical Korean IME row for replacing multiple formatted text nodes exposed two different failure modes in Slate v2 rich text:
NotFoundError: Failed to execute 'removeChild' on 'Node'The visible result was duplicated text like 가나다가나다, or unformatted DOM text
that was not present in Slate state.
editor.get.modelText()
still contains the old model text.Keep the ownership split explicit:
compositionstartThe proof now uses Chromium CDP Input.imeSetComposition, mirroring Lexical's
native route, and asserts DOM text, Slate model text, mark preservation, caret,
and composition trace ownership.
IME replacement across formatted leaves is not just "insert text". The browser owns a temporary composition region, but Slate owns the committed model. During trusted native IME, deleting the model selection before commit makes the later native insert land at the right logical position. During synthetic DOM proofs, pre-delete is wrong because the test already mutates the live DOM range.
The cleanup step is needed because Chromium can mutate an existing managed text node for the live composition, while the committed Slate model renders the final text in a different marked leaf. Resetting only managed strings whose DOM text is the model text plus the committed composition text removes the browser-owned tail without touching the actual committed Slate text.
For Slate v2 IME proofs:
compositionend using the latest
composition update text when compositionend.data is emptybun playwright test playwright/integration/examples/richtext.test.ts --project=chromium --grep "syncs browser text mutations inside bold markup|commits IME composition inside bold markup|replaces multiple formatted text nodes with Korean IME composition" --retries=0bun playwright test playwright/integration/examples/rendering-strategy-runtime.test.ts --project=chromium --grep "commits IME composition through|records runtime metadata for committed IME composition|undoes committed IME composition as one history step|does not push canceled IME composition onto history|drops active IME composition when a model change overlaps it|drops active IME composition when a model change partially overlaps it|drops active IME composition when a model change happens at its insertion point|keeps active IME composition when a model change happens elsewhere|commits rapidly following IME compositions in separate text blocks|commits cross-paragraph IME composition as one replacement" --retries=0bun run lint:fix packages/slate-react/src/editable/composition-state.ts packages/slate-react/src/editable/mutation-controller.ts playwright/integration/examples/richtext.test.tsbun typecheck:root