[lexical] Bug Fix: Normalize non-inline nodes when inserting into inline-only parents#8715
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
3e5362b to
737eef8
Compare
737eef8 to
474c445
Compare
474c445 to
8180d2d
Compare
8180d2d to
fba6381
Compare
fba6381 to
045d6af
Compare
045d6af to
f51337a
Compare
f51337a to
da4aa58
Compare
bd6c15a to
e2d7f39
Compare
e2d7f39 to
fd30e9a
Compare
fd30e9a to
a4eaec0
Compare
a4eaec0 to
e147217
Compare
e147217 to
4aef611
Compare
4aef611 to
2a7b35c
Compare
…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
2a7b35c to
f285534
Compare
zurfyx
approved these changes
Jun 21, 2026
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
RangeSelection.insertNodescould throw, or produce an invalid documentstructure, when the destination block cannot legally contain the inserted
non-inline content.
DecoratorNodeinto the block cursor inside anElementNodethrew an exception (Bug: Inserting a DecoratorNode into the block cursor inside an ElementNode throws an exception #8713).HorizontalRuleNode) into an inline-only parent such as a collapsible title produced an invalid tree: a non-inline child nested inside an element that may only contain inline children (Bug: Paste doesn't properly normalize non-inline nodes when the destination shouldn't contain them #8724).The document structure rules being enforced are:
RootNodeand shadow-rootElementNodes may only have non-inline children.ElementNodes may only contain inline children.ElementNodes may only contain inline children.ListNode/ListItemNodehave their own containment rules and relocate themselves viaisParentRequired().This PR makes
insertNodeskeep 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: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.ElementNodewhose 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
lexicalcore package and requires no changes to node implementations or their extensions.This PR also includes preparatory/hygiene changes:
$insertNodeToNearestRootAtCaretfrom@lexical/utilsinto thelexicalcore package (caret/LexicalCaretUtils), exports it fromlexical, and re-exports it from@lexical/utilsfor backwards compatibility. This letsinsertNodesuse the helper without a circular dependency on@lexical/utils.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. Theexamples/projects are intentionally left on the published-package imports.Closes #8713
Closes #8724
Test plan
Before
/divider, Enter → Backspace, Cut →/collapsible, Enter → Paste) crashes / leaves the editor with anon-inline
HorizontalRuleNodenested inside the inline-onlyCollapsibleTitleNode.DecoratorNodeinto a block cursor inside anElementNodethrows.
After
Issue8724Reprounit tests cover the no-block and inline-only-parentcases with shadow-root, inline-only element, and list parents, using
decorator and element nodes.
Issue8724Collapsibleplayground unit test reproduces the paste-into-collapsible-title repro and asserts the collapsible is left intact and the
block is dropped.
tsc,eslint, andprettierare clean across the workspace; full unitand e2e suites (including the lists and copy/paste suites) pass.