Back to Plate

Slate React beforeinput delete commands must refresh synced selection

docs/solutions/ui-bugs/2026-04-30-slate-react-beforeinput-delete-commands-must-refresh-synced-selection.md

53.0.63.2 KB
Original Source

Slate React beforeinput delete commands must refresh synced selection

Problem

slate-react cleanup threaded the typed kernel command into model-owned beforeinput execution so insert commands no longer reparsed stale raw event data. That was correct for text insertion, but delete command shape depends on the current selection after DOM selection import.

Symptoms

  • A prepared deleteContentBackward command could be computed while Slate still had a collapsed selection.
  • syncSelectionForBeforeInput could then import an expanded DOM selection, but execution still used the stale prepared single-character delete command.
  • The first focused test proved typed insert commands won over stale event data, but did not cover delete command shape after selection sync.

What Didn't Work

  • Treating every prepared command as final. Insert command payloads should be final, but delete commands derive their shape from the latest selection.
  • Going back to full reparsing for every command. That would lose the point of the typed input kernel and reintroduce stale raw event data as an authority.

Solution

Keep prepared commands authoritative for non-delete input, but refresh delete commands from the synced selection:

ts
const parsedCommand = () =>
  getEditableCommandFromBeforeInputType({
    data,
    inputType: type,
    selection,
  })

const command =
  preparedCommand === undefined || type.startsWith('delete')
    ? (parsedCommand() ?? preparedCommand ?? null)
    : preparedCommand

Lock both sides with tests:

ts
applyModelOwnedBeforeInputOperation({
  command: { inputType: 'insertText', kind: 'insert-text', text: 'kernel' },
  data: 'event',
  inputType: 'insertText',
})

and:

ts
applyModelOwnedBeforeInputOperation({
  command: { direction: 'backward', kind: 'delete' },
  inputType: 'deleteContentBackward',
  selection: expandedSelection,
})

The insert row asserts the prepared command wins over stale event data. The delete row asserts synced expanded selection upgrades the command to delete-fragment.

Why This Works

Text insertion authority lives in the typed command payload. Delete command authority lives in both the input type and the selection shape. beforeinput selection sync sits between kernel preparation and model execution, so delete commands must be the narrow exception to "prepared command wins."

Prevention

  • Any input-kernel cleanup must classify commands by which fields are stable before DOM selection import.
  • Add paired tests for prepared payload authority and selection-dependent command refresh.
  • Keep the focused browser row for persistent native word-delete in the proof bundle after this kind of refactor.