Skip to content

[lexical] Bug Fix: Normalize non-inline nodes when inserting into inline-only parents#8715

Merged
etrepum merged 4 commits into
facebook:mainfrom
etrepum:claude/lexical-issue-8713-zrve75
Jun 21, 2026
Merged

[lexical] Bug Fix: Normalize non-inline nodes when inserting into inline-only parents#8715
etrepum merged 4 commits into
facebook:mainfrom
etrepum:claude/lexical-issue-8713-zrve75

Conversation

@etrepum

@etrepum etrepum commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Description

RangeSelection.insertNodes could throw, or produce an invalid document
structure, when the destination block cannot legally contain the inserted
non-inline content.

The document structure rules being enforced are:

  • RootNode and shadow-root ElementNodes may only have non-inline children.
  • Other non-inline ElementNodes may only contain inline children.
  • Inline ElementNodes may only contain inline children.
  • ListNode/ListItemNode have their own containment rules and relocate themselves via isParentRequired().

This PR makes insertNodes keep the tree valid by splitting/relocating, or dropping content that has no valid inline form, instead of crashing or nesting nodes illegally. Two cases are added:

  • No enclosing block (firstBlock === null) — each top-level inserted block is placed relative to the nearest root using $insertNodeToNearestRootAtCaret, rather than splicing a block into a context that cannot hold it.
  • Inline-only enclosing block — when the enclosing block is a non-inline, non-shadow-root ElementNode whose parent is not a root/shadow root and which does not require a specific parent, the inserted nodes are reduced to their inline content and spliced in. Non-inline nodes with no inline form (e.g. HorizontalRuleNode) are dropped rather than nested illegally. isParentRequired() leaves list structures on the existing code path so list paste behavior is unchanged.

The fix lives entirely in the lexical core package and requires no changes to node implementations or their extensions.

This PR also includes preparatory/hygiene changes:

  • Moves $insertNodeToNearestRootAtCaret from @lexical/utils into the lexical core package (caret/LexicalCaretUtils), exports it from lexical, and re-exports it from @lexical/utils for backwards compatibility. This lets insertNodes use the helper without a circular dependency on @lexical/utils.
  • Switches internal consumers to import core re-exports directly from lexical (e.g. $insertNodeToNearestRootAtCaret, addClassNamesToElement, removeClassNamesFromElement, mergeRegister, $findMatchingParent, isHTMLElement, isHTMLAnchorElement, $splitNode, isBlockDomNode, isInlineDomNode, $getAdjacentSiblingOrParentSiblingCaret), and replaces relative test imports with package imports where the symbol is public. The examples/ projects are intentionally left on the published-package imports.

Closes #8713
Closes #8724

Test plan

Before

  • Following the repro (clear editor → /divider, Enter → Backspace, Cut →
    /collapsible, Enter → Paste) crashes / leaves the editor with a
    non-inline HorizontalRuleNode nested inside the inline-only
    CollapsibleTitleNode.
  • Inserting a DecoratorNode into a block cursor inside an ElementNode
    throws.

After

  • New Issue8724Repro unit tests cover the no-block and inline-only-parent
    cases with shadow-root, inline-only element, and list parents, using
    decorator and element nodes.
  • New Issue8724Collapsible playground unit test reproduces the paste-into-
    collapsible-title repro and asserts the collapsible is left intact and the
    block is dropped.
  • tsc, eslint, and prettier are clean across the workspace; full unit
    and e2e suites (including the lists and copy/paste suites) pass.
@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 Jun 17, 2026
@vercel

vercel Bot commented Jun 17, 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, Comment Jun 20, 2026 4:37pm
lexical-playground Ready Ready Preview, Comment Jun 20, 2026 4:37pm

Request Review

@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from 3e5362b to 737eef8 Compare June 17, 2026 20:42
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from 737eef8 to 474c445 Compare June 17, 2026 22:14
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from 474c445 to 8180d2d Compare June 19, 2026 06:34
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from 8180d2d to fba6381 Compare June 19, 2026 15:09
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from fba6381 to 045d6af Compare June 19, 2026 16:05
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from 045d6af to f51337a Compare June 19, 2026 16:16
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from f51337a to da4aa58 Compare June 19, 2026 16:19
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jun 19, 2026
@etrepum etrepum marked this pull request as ready for review June 19, 2026 16:29
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from bd6c15a to e2d7f39 Compare June 20, 2026 05:43
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from e2d7f39 to fd30e9a Compare June 20, 2026 05:54
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from fd30e9a to a4eaec0 Compare June 20, 2026 05:56
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from a4eaec0 to e147217 Compare June 20, 2026 06:08
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from e147217 to 4aef611 Compare June 20, 2026 06:32
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from 4aef611 to 2a7b35c Compare June 20, 2026 06:43
claude added 3 commits June 20, 2026 15:35
…cal core

**Description**

Moves `$insertNodeToNearestRootAtCaret` from `@lexical/utils` into the
`lexical` core package (in `caret/LexicalCaretUtils`), exports it from
`lexical`, and re-exports it from `@lexical/utils` for backwards
compatibility with external consumers.

This is a pure relocation with no behavioral change. It is a prerequisite
for upcoming work in `RangeSelection.insertNodes`, which can be simplified
by using this caret helper directly from core without pulling in a
dependency on `@lexical/utils`.

**Closes**

N/A (preparatory refactor)

**Test plan**

- `tsc` is clean across the workspace.
- Existing unit and e2e suites continue to pass; the `@lexical/utils`
  re-export keeps all current consumers working unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XceB616N8r2krnQ8Age9uz
**Description**

Mechanical import cleanups across the monorepo (excluding the `examples/`
projects, which intentionally track the latest published release):

- Switch internal consumers from `@lexical/utils` to importing the
  equivalent core re-exports directly from `lexical`
  (`$insertNodeToNearestRootAtCaret`, `addClassNamesToElement`,
  `removeClassNamesFromElement`, `mergeRegister`, `$findMatchingParent`,
  `isHTMLElement`, `isHTMLAnchorElement`, `$splitNode`, `isBlockDomNode`,
  `isInlineDomNode`, `$getAdjacentSiblingOrParentSiblingCaret`).
- Replace relative test imports (`../..`, `../../<module>`) with package
  imports wherever the symbol is part of a package's public interface,
  keeping relative imports only for genuinely internal symbols.

No behavioral changes; this is purely an import hygiene pass.

**Closes**

N/A (chore)

**Test plan**

- `tsc` is clean across the workspace.
- `eslint` and `prettier` are clean.
- Unit and e2e suites pass unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XceB616N8r2krnQ8Age9uz
…nly parents

**Description**

`RangeSelection.insertNodes` could produce an invalid document structure
(or throw) when the destination block cannot legally contain the inserted
non-inline content. Two cases are now handled:

- When there is no enclosing block (`firstBlock === null`), each top-level
  inserted block is placed relative to the nearest root using
  `$insertNodeToNearestRootAtCaret`, which keeps the tree valid instead of
  splicing a block into a context that cannot hold it.
- When the enclosing block is an inline-only element (a non-inline,
  non-shadow-root `ElementNode` whose parent is not a root/shadow root, and
  which does not require a specific parent), the inserted nodes are reduced
  to their inline content and spliced in. Non-inline nodes with no inline
  form (e.g. a `HorizontalRuleNode`) are dropped rather than nested
  illegally. `isParentRequired()` is used to leave list structures (which
  relocate themselves) on the existing code path.

This fixes the reported crash when inserting a `DecoratorNode` into the
block cursor inside an `ElementNode` (facebook#8713), as well as the invalid
structure produced when pasting a non-inline node into an inline-only
parent such as a collapsible title (facebook#8724).

**Closes** facebook#8713, facebook#8724

**Test plan**

- New `Issue8724Repro` unit tests cover the no-block and inline-only-parent
  cases (including shadow-root, inline-only element, and list parents) with
  decorator and element nodes.
- New `Issue8724Collapsible` playground unit test reproduces the original
  paste-into-collapsible-title repro and asserts the collapsible is left
  intact and the block is dropped.
- `tsc`, `eslint`, and `prettier` are clean; full unit and e2e suites pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XceB616N8r2krnQ8Age9uz
@etrepum etrepum force-pushed the claude/lexical-issue-8713-zrve75 branch from 2a7b35c to f285534 Compare June 20, 2026 15:37
@etrepum etrepum changed the title [lexical] Bug Fix: Insert DecoratorNode at a block cursor inside an ElementNode Jun 20, 2026
@etrepum etrepum marked this pull request as ready for review June 20, 2026 16:36
@etrepum etrepum mentioned this pull request Jun 25, 2026
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

4 participants