{{ message }}
Repository navigation
[lexical] Bug Fix: $removeTextFromCaretRange no longer leaves a childless list or table as the root's only child - #9116
Merged
etrepum merged 1 commit intoSep 8, 2026
Conversation
…less list or table as the root's only child The empty ancestor walk stopped at the root's last child even when that child declared canBeEmpty() false, so select all delete over [block decorator, list] left root[list[]] and the next inline insert threw out of insertNodes. I added canBeEmpty() to the break condition, so the container is removed and the restoration below supplies the paragraph. Tables change too; an empty one took the insert as a child, not rows.
Om-singhaI
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
September 4, 2026 17:24
etrepum
approved these changes
Sep 8, 2026
Merged
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
Current behavior: in a document whose first block is a block decorator and whose last is a list, Cmd+A then Backspace leaves the root holding a
ListNodewith no children, and the next inline insert throwsExpected node ListNode of type list to have a block ElementNode ancestorout ofinsertNodes. The invariant is uncaught, so it reaches the application'sonError.Expected, and what this produces:
root[paragraph], with the next Shift+Enter inserting normally.Mechanism
$removeTextFromCaretRangeremoves the emptied focus block, then walks up removing ancestors emptied with it. The walk breaks when removingparentwould empty the root:That applies unconditionally, so it stops on a container that has declared it cannot be empty. The fix adds
&& parent.canBeEmpty().Three things say the guard is stale. Main has no policy of preserving lists, only one about the root's child count: the same keystrokes over
[paragraph, list]remove the list today, because the root then has two children and the guard never fires. The sibling branch of the same function does the same climb with the comment// remove empty parent node even if parent node is canBeEmptyand no guard. And the case the guard protects is now handled below it by$restoreEmptyContainerParagraph, which arrived in #9058 two months after the guard arrived in #8751.This is not specific to lists
TableNodeand the playground'sLayoutContainerNodealso returncanBeEmpty(): falseand can sit under the root, so[block decorator, table]goes fromroot[table[]]toroot[paragraph[]]too. The table failure is quieter than the list one: it does not throw, becauseinsertNodescatches an element point on a shadow root first, and the content is spliced into theTableNodeinstead, giving a table holding a paragraph where its rows belong. One rule looks right for both and both are tested, but happy to split the table half out.This is visible to consumers: any third party
ElementNodereturningcanBeEmpty(): falsethat can be a direct child of the root now disappears on this path instead of surviving empty.Test plan
Six tests in
LexicalCaret.test.ts. Four cover the changed branch, a list and a table each replaced with an empty paragraph plus the follow up insert for each. Two are controls that pass in either state.a container that may be empty is keptis the control that bears on the guard:TestElementNodeat the same depth reaches the guard and takes the preserved branch, and replacing the new conjunct withfalseflips it. Nothing else pins that branch,Issue8745Repro.test.tsfrom #8751 still passes all 14 with the break neutralized, which is part of why the guard reads as stale.Reverting only
LexicalCaretUtils.ts: 4 failed, 179 passed. Restoring it: 183 passed. Neighbouring suites each pass unchanged, includingIssue8745Repro14,LexicalSelection100,LexicalLinkNode74 andLexicalTableNode42.tsc --noEmit,eslintandprettier --checkclean.I did not run the browser project, so
Issue8745DeleteLine.test.tsfrom #8751 is unexercised. Every element registered there returnscanBeEmpty()true, so the added conjunct is a no op and the break expression is identical to main, but that is an argument from the node definitions rather than a run.