[lexical-table] Bug Fix: Odd selection behavior with table as last element in document - #8858
[lexical-table] Bug Fix: Odd selection behavior with table as last element in document#8858Jynx2004 wants to merge 18 commits into
Conversation
…hadow root in table selection
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
The fix should confirm we're at a dead end after this table: if (direction === 'forward') {
const focusNode = selection.focus.getNode();
if (
$isRootOrShadowRoot(focusNode) &&
selection.focus.offset === focusNode.getChildrenSize() &&
focusNode.getLastChild() === tableNode
) {
stopEvent(event);
return true;
}
}
Aside from that — the feedback on #8030 wasn't just about moving to the table plugin. The inline comments showed a precision pattern: the backward case right below checks Some other things:
The cursor geometry here is different from the backward case (root-level element selection vs cursor inside a paragraph), but the principle is the same: verify the table relationship before stopping the event. |
Hey @mayrang i think the code you wrote is more reliable . Just one thing is that getLastChild function is deprecated so if we remove that condition i think it still works fine with the first 2 conditions to check the dead end. |
|
|
|
|
Absolutely not. |
Co-authored-by: Bob Ippolito <bob@redivi.com>
etrepum
left a comment
There was a problem hiding this comment.
This PR still needs tests before it can be considered for merge. There is nothing that shows what it's supposed to do or tests that it works.
Will add some tests to confirm the behavior |
Added added a test case to check the expected behavior |
etrepum
left a comment
There was a problem hiding this comment.
Here are some notes from Claude on the implementation and current approach, overall it looks pretty good but a few things can be tightened up
Review: PR #8858 — Odd selection behavior with table as last element
✅ Verification: tests fail without the fix
I applied the PR's two files and ran the new test both ways:
| State | Result |
|---|---|
| Fix + test applied | 8/8 pass |
| Test applied, source fix reverted | 1 fail — ArrowRight at root end after a table… fails at expect(secondHandled).toBe(true) (received false) |
The test genuinely exercises the change. One nuance: only the second ArrowRight assertion depends on the fix — the first firstHandled returns true from pre-existing code even without the change, so the new branch's coverage rests entirely on the secondHandled assertion.
The fix itself is narrow and correct: when a range selection's focus is at the root/shadow-root end (offset === childrenSize) and the last child is this tableNode, forward navigation is swallowed so the caret stays beneath the table — matching the expected behavior in #7999. Using tableNode.is(...) (key comparison) is the right choice for node versioning.
Findings (all minor — none blocks the fix)
1. Missing isCollapsed() guard on the forward branch — LexicalTableSelectionHelpers.ts:2142 (correctness)
The new block returns true/stopEvent whenever focus is at the root end after the table, without checking the selection is collapsed. A non-collapsed range selection (anchor before the table, focus at root offset === childrenSize) would normally collapse to its edge on ArrowRight; now it's swallowed. Shift+ArrowRight is likewise blocked from extending. Narrow, but reachable via block/table-level range selections. Consider gating on selection.isCollapsed().
2. Test placed under the wrong describe — LexicalTableSelectionHelpers.test.ts:372 (test organization)
The new test is nested inside describe('DELETE_LINE_COMMAND in table cells'), so it reports under an unrelated heading. Suggest moving it to its own block, e.g. describe('regression #7999').
3. Forward-only fix — LexicalTableSelectionHelpers.ts:2136 (altitude/completeness)
Only direction === 'forward' is handled. The issue repro is right-arrow-specific so scope is defensible, but ArrowDown out of the last cell reaches the same root-end position with no equivalent stop. Generalizing "caret at root end after last-child table" would be a deeper fix than a forward-only special case.
4. Redundant guard / eager computation — LexicalTableSelectionHelpers.ts:2138 (simplification)
$isRootOrShadowRoot(focusNode) in the guard already implies focusNode is an ElementNode, so the $isElementNode(...) ? getLastChild() : null ternary is dead on the meaningful path and evaluates getLastChild() even for non-root focus nodes. Compute getLastChild() after confirming root:
if (direction === 'forward' && $isRootOrShadowRoot(focusNode)) {
const focusNode = selection.focus.getNode();
if (
$isRootOrShadowRoot(focusNode) &&
selection.focus.offset === focusNode.getChildrenSize() &&
tableNode.is(focusNode.getLastChild())
) {
stopEvent(event);
return true;
}
}
Sure |
I have fixed 1st and 2nd issues . Regarding the 3rd point , here we are facing the issue only in the case of right arrow command and no other command . So this issue limits to only forward direction. |
|
There is a similar quirk with arrow down that we should also fix here, if you press down from the end of the document and then left or up then the selection does not land in the last table cell |
I think this issue lies in the below code snippet This makes SELECTION_CHANGE_COMMAND run two times on chromium as well where it should only run on firefox . It must be guarded by using Updated code - This fixes the issue . Would it be possible if i make a seperate PR for this issue ? |
|
It would be better to consolidate all of the known arrow key workarounds into one PR |
Description
Screen.Recording.2026-07-18.152021.mp4
Closes #7999