fix: dispatch grapheme delete on live view in web composer

The Backspace handler computed positions from the `view` it was handed
(`view.state.selection`, `view.state.doc`) but executed the delete against
the closed-over React `editor`. When the editor is recreated (e.g. theme
toggle resets the instance and doc), `editor.state.doc` is briefly a
smaller, mismatched document, so a position valid in `view` overflows it -
ProseMirror throws "Position N out of range" (APP-SBSP: ~87k events).
Dispatch on the `view` the handler received so positions and document come
from the same state, and clamp to docSize as a backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-06-09 16:25:42 +03:00
parent 2cb225dddd
commit a7c01a287b
@@ -231,10 +231,19 @@ export function TextInput({
// otherwise, delete the last grapheme using deleteRange,
// so that emojis are deleted as a whole
const deleteFrom = cursorPosition - lastGrapheme.length
editor?.commands.deleteRange({
from: deleteFrom,
to: cursorPosition,
})
// Resolve positions against `view`, not the closed-over
// `editor`. The `editor` ref can briefly point at a stale
// instance (e.g. after the editor is recreated on a theme
// change or content reset) whose document is out of sync
// with the live `view`, causing deleteRange to throw
// "Position X out of range". Clamp to the live doc as a
// final guard. See APP-SBSP -sfn
const docSize = view.state.doc.content.size
const from = Math.max(0, Math.min(deleteFrom, docSize))
const to = Math.max(from, Math.min(cursorPosition, docSize))
if (from < to) {
view.dispatch(view.state.tr.delete(from, to))
}
return true
}
}