Skip to content

[lexical] Bug Fix: collapse to a paragraph on select-all delete of a non-paragraph first block - #8884

Open
gunjanjaswal wants to merge 4 commits into
facebook:mainfrom
gunjanjaswal:fix/5835-select-all-delete-non-paragraph-first-block
Open

[lexical] Bug Fix: collapse to a paragraph on select-all delete of a non-paragraph first block#8884
gunjanjaswal wants to merge 4 commits into
facebook:mainfrom
gunjanjaswal:fix/5835-select-all-delete-non-paragraph-first-block

Conversation

@gunjanjaswal

@gunjanjaswal gunjanjaswal commented Jul 27, 2026

Copy link
Copy Markdown

Description

When you select all and delete, the document should collapse to a single empty paragraph, the same as an empty editor. It does when the first block is a paragraph, but when the only remaining block is a heading, quote, list, or nested list, that block survives empty and keeps its type instead of becoming a paragraph.

RangeSelection.collapseAtStart already covers backwards deletion of a heading or quote, but not forward Delete or the list cases. This adds a targeted collapse in RangeSelection.deleteCharacter: after a full-range delete leaves the selection collapsed, if the root's only content is a single empty non-paragraph block sitting on a single empty branch, it is replaced with a fresh empty paragraph. The guard is deliberately tight, so any sibling content, decorator, shadow root, or surviving text leaves the block untouched.

It lives in deleteCharacter rather than the shared removeText / $removeTextFromCaretRange primitive on purpose. insertText also routes through removeText to clear the selection before inserting (the Prettier "format" flow does this on a lone code block), and that replacement has to keep its block type.

Closes #5835

Test plan

Before

Select all + delete with a heading, quote, list item, or nested list as the only block left an empty block of that type instead of a paragraph, and there was no coverage for it.

After

Added unit tests in LexicalCaret.test.ts covering select-all + delete, both Backspace and forward Delete, with the only block being a heading, quote, list item, and a nested list. Each collapses to a single empty paragraph. Controls confirm the paragraph-first case and a partial (non-full-range) delete are unaffected. The new behavior tests fail on main without the source change. Unit tests for the affected packages, tsc, and eslint all pass.

…raph first block

When a range deletion removes all root content and the surviving block is a non-paragraph element block (list item, heading, quote, or nested list), it lingered empty while keeping its element type instead of collapsing to a plain paragraph.

Add a tight special-case in $removeTextFromCaretRange: once the removal settles, if the sole remaining root content is a single empty non-paragraph block whose ancestor chain to the root is a single empty branch, replace that top-level subtree with a fresh empty paragraph. Shadow roots, decorators, and any sibling content bail out so the shared deletion path is otherwise untouched.

Closes facebook#5835
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 27, 2026
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview Jul 30, 2026 1:27am
lexical-playground Ready Ready Preview Jul 30, 2026 1:27am

Request Review

@etrepum etrepum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't pass tests.

The previous approach put the "empty document collapses to a paragraph"
special case inside $removeTextFromCaretRange. That primitive is shared:
insertText() also calls removeText() to clear the old selection before
inserting, so replacing all of a lone non-paragraph block's content (for
example the Prettier "format" button on a single code block) was wrongly
collapsing the block to a paragraph, and the reformatted text landed in a
plain paragraph. That broke the CodeActionMenu e2e test.

Move the logic to RangeSelection.deleteCharacter, the actual delete-key
path, so replacements are untouched. The existing collapseAtStart handling
there only covers backwards deletion of a heading or quote; extend it to
also cover forward Delete and list / nested-list survivors. The guard stays
tight: it only fires when a full-range delete leaves the root with a single
empty non-paragraph block on a single empty branch, bailing on any sibling
content, decorator, or shadow root.

Revert the $removeTextFromCaretRange change and rewrite the unit tests to
drive the behavior through deleteCharacter (both directions), plus a
regression test that replacing all text in a lone block keeps its type.

Closes facebook#5835
@gunjanjaswal

Copy link
Copy Markdown
Author

Good catch, that was a real bug on my end, not a flake. The collapse-to-paragraph was sitting in $removeTextFromCaretRange, but insertText clears the old selection through removeText too, so the Prettier format flow on a lone code block was getting collapsed into a paragraph before the reformatted text was inserted, which is what timed out the CodeActionMenu test.

I moved it to deleteCharacter where the existing collapseAtStart handling already lives, extended that to cover forward Delete and list survivors, and reverted the change to the caret primitive. The tests now drive it through the delete path in both directions, plus a regression test that replacing all text in a lone block keeps its type. Rebased on current main; the unit suite and the e2e specs around select-all/delete are green locally.

@etrepum

etrepum commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

The PR title and description should follow the template https://raw.githubusercontent.com/facebook/lexical/refs/heads/main/.github/pull_request_template.md

@gunjanjaswal gunjanjaswal changed the title fix: collapse to paragraph when select-all delete empties a non-paragraph first block [lexical] Bug Fix: collapse to a paragraph on select-all delete of a non-paragraph first block Jul 30, 2026
@gunjanjaswal

Copy link
Copy Markdown
Author

Thanks, updated the title and description to match the template (packages tag + PR type in the title, Closes #5835 and a Before/After test plan in the body). CI is green now as well. Happy to adjust anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: select all + delete should remove all blocks

2 participants