toSlateRange: forward-unhang guard discards a selection end point inside a block void
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:
// 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
- Document:
paragraph/ blockvoid/paragraph. - Put the caret in the first paragraph.
- 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-dom0.117.4 and 0.124.1 (guard is byte-identical in both:dist/index.js:1226and:1304respectively) - 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:
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.
Source: ianstormtaylor/slate