docs/solutions/logic-errors/2026-04-27-slate-positions-must-group-character-navigation-by-text-block-boundaries.md
/examples/tables moved the caret from an empty first cell to offset 1 in
the second cell after ArrowRight. The visible browser bug came from the core
position iterator, not from the table example.
ArrowRight produced Slate
selection [1, 0, 1, 0]@1."Human" at offset 1.[1, 0, 1, 0]@0.Teach packages/slate/src/editor/positions.ts to group position segments by
the nearest non-inline element that actually owns inline/text content:
const getTextBlockPath = (editor: Editor, path: Path): Path => {
for (let depth = path.length - 1; depth > 0; depth--) {
const candidatePath = path.slice(0, depth) as Path
const node = Editor.getLiveNode(editor, candidatePath)
if (
node &&
Node.isElement(node) &&
!Editor.isInline(editor, node) &&
Editor.hasInlines(editor, node)
) {
return candidatePath
}
}
return path[0] == null ? [] : ([path[0]] as Path)
}
Then have groupPositionSegmentsByBlock(...) and
collectBlockBoundaryPoints(...) compare segment.groupPath instead of
assuming segment.path[0] is the text-block boundary.
Guard it in two places:
Editor.after(..., { unit: "character" }) across nested
table-cell text blocks returns the next cell at offset 0/examples/tables click first cell, press ArrowRight,
assert the selection is [1, 0, 1, 0]@0Character iteration intentionally avoids yielding the start of every split text segment inside one text block. That is correct for inline fragments like links inside a paragraph.
A table is different. Its top-level node contains many independent text-blocks
inside cells. Grouping by path[0] flattened the whole table into one logical
text stream, so moving into the next cell skipped that cell's start point and
landed after the first character.
Grouping by the nearest text-block-owning element preserves inline-fragment behavior inside paragraphs while restoring block-boundary starts for nested structures.