[lexical] Bug Fix: collapse to a paragraph on select-all delete of a non-paragraph first block - #8884
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…-delete-non-paragraph-first-block
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
|
Good catch, that was a real bug on my end, not a flake. The collapse-to-paragraph was sitting in I moved it to |
|
The PR title and description should follow the template https://raw.githubusercontent.com/facebook/lexical/refs/heads/main/.github/pull_request_template.md |
|
Thanks, updated the title and description to match the template (packages tag + PR type in the title, |
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.collapseAtStartalready covers backwards deletion of a heading or quote, but not forward Delete or the list cases. This adds a targeted collapse inRangeSelection.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
deleteCharacterrather than the sharedremoveText/$removeTextFromCaretRangeprimitive on purpose.insertTextalso routes throughremoveTextto 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.tscovering 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 onmainwithout the source change. Unit tests for the affected packages,tsc, and eslint all pass.