#6109·slate

toSlateRange: forward-unhang guard discards a selection end point inside a block void

Author: ivan-zatravkin-quantoriCreated Sep 17, 2026Updated Sep 17, 2026

Description

DOMEditor.toSlateRange discards a valid selection end point that lands inside a block void.

At the end of toSlateRange there is a guard that unhangs a forward range ending in a void:

javascript
// if the selection is a hanging range that ends in a void
// and the DOM focus is an Element
// (meaning that the selection ends before the element)
// unhang the range to avoid mistakenly including the void
if (Range.isExpanded(range) && Range.isForward(range) && isDOMElement(focusNode) &&
    Editor.void(editor, { at: range.focus, mode: 'highest' })) {
  range = Editor.unhangRange(editor, range, { voids: true })
}

The comment states the inference the guard rests on: the DOM focus is an Element, meaning that the selection ends before the element. That inference does not hold when the focus Element is a descendant of the void itself, which is what Chrome produces for any downward gesture onto a block void — the focus node is the void's own content wrapper or its data-slate-spacer, not a node before the void.

toSlatePoint resolves that DOM point correctly (its voidNode branch returns the void's own text point), and the guard then throws the correct point away. The result is that a forward selection can never end on a block void, while the identical backward selection can, because Range.isForward is false and the guard does not fire.

Recording Bugs

Not recorded — this needs a block void with a non-editable inner wrapper, which the hosted sandbox does not have by default. It reproduces in the horizontal-rule shape used by the docs: a block void that renders contentEditable={false} on an inner element and {children} (the spacer) outside it.

Steps

  1. Document: paragraph / block void / paragraph.
  2. Put the caret in the first paragraph.
  3. Either drag downward with the mouse and release on the void, or press Shift+↓.

Expectation

The range ends inside the void — { anchor: [0,0]:0, focus: [1,0]:0 } — the same shape that the equivalent upward gesture already produces.

Result

The range is unhung back into the previous block — { anchor: [0,0]:0, focus: [0,0]:14 }. The void is not part of the selection and cannot be made part of it by any downward gesture.

Environment

  • Slate: slate-dom 0.117.4 and 0.124.1 (guard is byte-identical in both: dist/index.js:1226 and :1304 respectively)
  • Browser: Chrome (also checked with slate-react 0.117.1 and 0.124.0)
  • OS: Linux

Suggested fix

Require that the DOM focus is not inside the void it is about to exclude, which is exactly what the existing comment already assumes:

javascript
isDOMElement(focusNode) && !focusNode.closest('[data-slate-void="true"]') && Editor.void(...)

I verified this locally against both versions above: downward gestures then end on the block void, the backward direction is unchanged, and a gesture that genuinely ends before the void (DOM focus outside it) still unhangs as before.

Happy to open a PR with a test if the approach looks right.