[lexical] Bug Fix: insertNodes no longer leaves an empty paragraph after a block inserted at the end of a paragraph - #9099
Merged
etrepum merged 2 commits intoAug 27, 2026
Conversation
…ter a block inserted at the end of a paragraph
Om-singhaI
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
August 26, 2026 18:00
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
approved these changes
Aug 27, 2026
unix-max
pushed a commit
to unix-max/lexical
that referenced
this pull request
Aug 28, 2026
…ter a block inserted at the end of a paragraph (facebook#9099) Co-authored-by: Bob Ippolito <bob@redivi.com>
etrepum
pushed a commit
to etrepum/lexical
that referenced
this pull request
Aug 30, 2026
Resolves the one conflict in `RangeSelection.insertNodes`: `main` split the `insertedParagraph` merge into a nested `if`/`else if` in facebook#9099, so the `!shouldPreserveInsertedBlocks` guard this branch added moves onto the inner `if` and `main`'s new `else if (insertedParagraph.isEmpty())` fallback is kept.
Merged
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:
RangeSelection.insertNodestreats block content inconsistently at the same caret. With the caret at the end of a paragraph, pasting another paragraph or a quote merges cleanly, but pasting a blockDecoratorNode(the reporter's PullQuote, a YouTube embed, an image, a horizontal rule) or a container element (a list, a table) leaves a stray empty paragraph after the pasted node. Pasting the same nodes in the middle of a paragraph is correct. The contradiction lives inside one function:insertNodesfirst splits the target paragraph withinsertParagraph, then tries to merge the paragraph it split off into the last inserted block. That merge requires$isElementNode(lastInsertedBlock), and for a pasted block decorator the$findMatchingParent(nodeToSelect, INTERNAL_$isBlock)walk starts at the decorator itself, soINTERNAL_$isBlockadmits it,$isElementNoderejects it, and the cleanup is skipped. A container element such as aListNodefails the other clause instead: the split paragraph reportscanMergeWhenEmpty()false andINTERNAL_$isBlock(lastToInsert)is false because the list's first child is aListItemNode. The reporter noted it reproduces with "even regular Element nodes", which is this second clause. The result is deterministic structural noise, one empty paragraph after the pasted node, gone with one Backspace, but it should never appear.This change splits the conditional: when the merge branch does not apply and the paragraph split off by
insertParagraphis empty, it is removed. TheisEmpty()guard is what keeps the mid paragraph case intact, because there the split paragraph holds the text after the caret and must stay. Pasting at the start of a paragraph is likewise unchanged, since there the split paragraph holds the whole original text. Typing after a paste at the end still works: with the block decorator as the last child of its parent,$transferStartingElementPointToTextPointcreates a paragraph on demand, so removing the stray paragraph strands nothing.The same shape exists inside list items: pasting a block decorator at the end of a non empty
ListItemNodepreviously left a stray empty list item, and the split conditional removes it the same way.Two collateral notes:
SharedHistoryExtensiontest inpackages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsxasserted the stray paragraph. It inserts a block decorator withinsertNodesat the end of a paragraph, and three of its HTML assertions included the trailing<p><br></p>that this bug produced. Those assertions now expect the decorator as the last block, followed by the reconciler'sDECORATOR_BOUNDARY_ANCHOR_HTML(the invisible zero size<img>parked outside a trailing block decorator since Bug: DecoratorNode resets the selection of all content #8922, which the trailing paragraph previously made unnecessary). The undo and redo sequence the test exercises is untouched.INSERT_HORIZONTAL_RULE_COMMANDcalls$insertNodeToNearestRoot(inpackages/lexical-extension/src/HorizontalRuleExtension.tsandpackages/lexical-react/src/LexicalHorizontalRulePlugin.ts), which never entersinsertNodes, and the paste portions ofHorizontalRule.spec.mjstarget empty paragraphs, whereinsertNodescreates no split paragraph at all (shouldInsertis false). A scan of the other e2e specs that assert a trailing empty paragraph after a block found only command insertions through$insertNodeToNearestRoot(tables, horizontal rules), markdown shortcut transformers, Enter key behavior, and pastes into empty paragraphs, none of which reach the changed branch.Closes #9095
Test plan
New unit tests in
packages/lexical/src/__tests__/unit/LexicalSelection.test.ts(Regression tests for #9095) cover block decorator and list pastes at the end, middle and start of a paragraph, quote and paragraph pastes at the same end of paragraph caret as controls, plus typing after a paste at the end:The reporter's exact scenario is a new test in
packages/lexical-playground/__tests__/unit/PullQuoteNode.test.ts: pasting aPullQuoteNodewith the caret at the end of a paragraph leaves no empty paragraph (failed before this change).Before
The three new end of paragraph tests against the previous
LexicalSelection.ts, each expecting two root children and finding three, the third being the stray empty paragraph:The six other matrix tests and the typing test pass before and after, pinning the behavior that must not change.
After
vitest --project unit --project scripts-unit --no-watch:The one skipped test predates this change. Reverting only the
LexicalSelection.tschange (keeping the tests) brings back the three failures above, and the updatedSharedHistoryExtensionassertions fail as well, since they now expect the decorator on the trailing edge.tsc --noEmitis clean, andeslintandprettierreport nothing for the changed files. The browser, integration and e2e suites were not run.