Skip to content

[lexical][lexical-rich-text][lexical-utils] Bug Fix: copy and insert keep the original node's state - #9059

Merged
etrepum merged 6 commits into
facebook:mainfrom
LeSingh1:consol/split-insert-copy-state
Aug 29, 2026
Merged

[lexical][lexical-rich-text][lexical-utils] Bug Fix: copy and insert keep the original node's state#9059
etrepum merged 6 commits into
facebook:mainfrom
LeSingh1:consol/split-insert-copy-state

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description

Four bugs, one shape. An operation that copies or inserts a block
builds the new node by hand instead of deriving it from the node it came
from, so the original's mode, alignment, format or style is silently
dropped — or the split leaves an extra empty block behind. This is the same
defect class already seen in the insertNewAfter paths that construct a
node from scratch rather than reusing the original's state.

Test plan

Each fix brings its own unit tests: the new Issue8101Repro.test.ts
and ParagraphCollapseAtStart.test.ts under packages/lexical, two new
cases in LexicalUtilsInsertNodeToNearestRoot.test.tsx, and the new
QuoteInsertNewAfter.test.ts under packages/lexical-rich-text. The
quote tests run the same helper against a paragraph and against a quote and
require them to agree, so they are pinned to core's behaviour rather than to
a hardcoded expectation. #8975 additionally adds a Playwright case to
packages/lexical-playground/__tests__/e2e/Selection.spec.mjs; that spec
needs a built playground and was not run locally, so it is covered by CI's
e2e job rather than by the output below.

Supersedes #8924, #8944, #8975, #8976, #9028, consolidated per the review
feedback on #9027 and #9035.

…insert keep the original node's state

## Description

Five bugs, one shape. An operation that splits, copies or inserts a block
builds the new node by hand instead of deriving it from the node it came
from, so the original's mode, alignment, format or style is silently
dropped — or the split leaves an extra empty block behind. This is the same
defect class already seen in the `insertNewAfter` paths that construct a
node from scratch rather than reusing the original's state.

- `TextNode.splitText` (`packages/lexical/src/nodes/LexicalTextNode.ts`)
  copied format, style, detail and NodeState onto the parts it creates but
  not `__mode`. Splitting a token TextNode left a token head followed by
  normal-mode tails, which are editable and can be merged into neighbouring
  text by normalization. Every part now keeps the mode of the node that was
  split; segmented nodes keep their existing behaviour of being downgraded
  to normal. (facebook#8924)

- `ParagraphNode.extractWithChild`
  (`packages/lexical/src/nodes/LexicalParagraphNode.ts`) did not exist, so
  `ParagraphNode` inherited the default of `false` and copying a single,
  fully selected paragraph dropped the paragraph itself from both clipboard
  payloads — its alignment, indent and style had nowhere to live and the
  paste came out left aligned. `HeadingNode` and `QuoteNode` already
  override this. `ParagraphNode` now does too, but only when the selection
  covers the whole paragraph and that paragraph actually carries
  block-level state, so a plain paragraph still copies as the inline-only
  content existing clipboard consumers expect. (facebook#8944, fixes facebook#8101)

- `ParagraphNode.collapseAtStart` (same file) decided a paragraph was
  "empty" from `children[0]` alone. A first paragraph whose first text node
  is blank but whose later children carry the content — a leading space
  followed by a bold run — was treated as empty, so Backspace at its start
  removed the whole paragraph and lost the content; a non-text child such
  as a line break was discarded the same way. It now requires every child
  to be a text node and the paragraph's whole text content to be blank.
  Blank and empty first paragraphs still collapse exactly as before. (facebook#8975)

- `$insertNodeToNearestRoot` (`packages/lexical-utils/src/index.ts`) splits
  the enclosing blocks at the selection focus, so a focus at the end of a
  block left an empty right-hand half — a spurious empty paragraph or list
  item after the inserted node. That empty block is only wanted when the
  insertion point is at the very end of its nearest root, where it is the
  only place left for the selection to go, so a `$shouldSplit` now declines
  the empty `'last'` split when the new `$hasContentAfter` helper reports
  content still following the caret. (facebook#8976, fixes facebook#5433)

- `QuoteNode.insertNewAfter` (`packages/lexical-rich-text/src/index.ts`)
  built the continuation block by hand, named its `RangeSelection` argument
  `_` and carried across only the direction, so Enter at the end of a
  centred quote produced a left-aligned paragraph while the identical
  keystroke in a centred paragraph did not. It now carries the block format
  and style and the caret's pending text format and style, matching core
  `ParagraphNode.insertNewAfter`. (facebook#9028)

## Test plan

Each fix brings its own unit tests: `splitText keeps the token mode on
every part` in `LexicalTextNode.test.tsx`, the new `Issue8101Repro.test.ts`
and `ParagraphCollapseAtStart.test.ts` under `packages/lexical`, two new
cases in `LexicalUtilsInsertNodeToNearestRoot.test.tsx`, and the new
`QuoteInsertNewAfter.test.ts` under `packages/lexical-rich-text`. The
quote tests run the same helper against a paragraph and against a quote and
require them to agree, so they are pinned to core's behaviour rather than to
a hardcoded expectation. facebook#8975 additionally adds a Playwright case to
`packages/lexical-playground/__tests__/e2e/Selection.spec.mjs`; that spec
needs a built playground and was not run locally, so it is covered by CI's
e2e job rather than by the output below.

### Before

Source fixes reverted, new tests kept:

```
$ npx vitest run --project unit packages/lexical/src packages/lexical-utils packages/lexical-rich-text
 × splitText keeps the token mode on every part
 × keeps a first paragraph whose content follows a blank text node
 × keeps a first paragraph whose only other child is a line break
 × copying a whole aligned paragraph keeps the alignment in the Lexical clipboard payload
 × copying a whole aligned paragraph keeps the alignment in the HTML clipboard payload
 × copy a centered paragraph and paste it into an empty paragraph
 × keeps the block alignment and style, as ParagraphNode does
 × seeds the continuation block with the pending text format
 × insert in the end of paragraph that is followed by another block
 × insert in the end of the last nested list item

 Test Files  5 failed | 83 passed (88)
      Tests  10 failed | 1622 passed (1632)
```

### After

```
$ npx vitest run --project unit packages/lexical/src packages/lexical-utils packages/lexical-rich-text

 Test Files  88 passed (88)
      Tests  1632 passed (1632)

$ npx tsc --noEmit -p .
(clean)
```

Supersedes facebook#8924, facebook#8944, facebook#8975, facebook#8976, facebook#9028, consolidated per the review
feedback on facebook#9027 and facebook#9035.
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

@LeSingh1 is attempting to deploy a commit to the Meta Open Source Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Aug 16, 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 Aug 28, 2026 8:34pm
lexical-playground Ready Ready Preview Aug 28, 2026 8:34pm

Request Review

@etrepum

etrepum commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

I don't think all of these are the correct approach, I don't think the intended behavior is for splitting a token to create more tokens for example. mode should only be applied to the original node and anything else created from that should be normal unless the caller explicitly wants them to keep the mode. I had made a similar comment on the original #8924

Removes the change that carried a TextNode's mode onto every part of a
split, along with its test. The mode downgrade is intentional, so this
belonged in a separate discussion rather than in this PR.
@LeSingh1

Copy link
Copy Markdown
Contributor Author

You're right, and I should have taken the #8924 answer the first time. Dropped the splitText mode-propagation change and its test — LexicalTextNode.ts is out of the diff entirely. I don't have the call-site audit or the playground repro you asked for, so I'm not going to keep pushing on it.

What's left is the paragraph/quote/insertNodeToNearestRoot work, 8 files. 123 tests pass locally.

@etrepum

etrepum commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

A lint rule changed since this PR was opened so you'll need to apply pnpm run lint:fix to this (and probably your other PRs) in order to pass integrity checks

@etrepum etrepum changed the title [lexical][lexical-rich-text][lexical-utils] Bug Fix: split, copy and insert keep the original node's state [lexical][lexical-rich-text][lexical-utils] Bug Fix: copy and insert keep the original node's state Aug 29, 2026
@etrepum
etrepum added this pull request to the merge queue Aug 29, 2026
Merged via the queue into facebook:main with commit 0fea608 Aug 29, 2026
46 checks passed
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

3 participants