docs/solutions/ui-bugs/2026-05-07-slate-react-ime-replacement-undo-must-merge-native-predelete.md
The Lexical history row for retaining selection after undoing IME replacement
exposed a Slate v2 history split. Trusted Chromium IME over an expanded
selection deleted the selected model text at compositionstart, then the Chrome
compositionend fallback inserted committed text as a separate history entry.
b in ab with IME text produced aす.a instead of restoring ab.Track only the trusted native replacement path:
const EDITOR_TO_COMPOSITION_PREDELETE = new WeakSet<Editor>();
When compositionstart sees an expanded selection on a trusted native event,
delete the fragment and mark that editor as having a composition pre-delete.
When compositionend commits through the Chrome fallback, consume that flag and
insert with history merge metadata:
editor.update(
(tx) => {
tx.text.insert(text);
},
{ metadata: { history: { mode: "merge" } } },
);
The regression row lives in
.tmp/slate-v2/playwright/integration/examples/rendering-strategy-runtime.test.ts
as restores expanded selection after undoing IME replacement. It uses
Chromium CDP IME composition over a backward DOM selection and asserts text plus
selection after undo.
Trusted browser IME replacement is a two-phase edit from Slate's point of view: the model selection is removed before the committed text is inserted. Users see one replacement action, so history must store it as one undoable action. The WeakSet keeps that merge scoped to the exact replacement path and is cleared on composition end, so normal composition commits do not get silently coalesced.
bun 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|restores expanded selection after undoing IME replacement|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 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=webkit --grep "deletes shell-backed selection after WebKit compositionend" --retries=0bun playwright test playwright/integration/examples/richtext.test.ts --project=webkit --grep "deletes rich text (selection|line selection) after WebKit compositionend" --retries=0bun run lint:fix packages/slate-react/src/editable/composition-state.ts playwright/integration/examples/rendering-strategy-runtime.test.ts playwright/integration/examples/richtext.test.tsbun typecheck:root