[lexical][lexical-selection][lexical-list] Bug Fix: Skip named-slot values in block converters, diagnose replace() on a slot value, and delete slot-bearing decorator hosts at the selection boundary - #8905
Open
etrepum wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
marked this pull request as ready for review
August 2, 2026 03:57
etrepum
requested review from
acywatson,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
August 2, 2026 03:57
etrepum
force-pushed
the
claude/elementnode-replace-audit-ahvm8e
branch
from
August 2, 2026 04:25
1d47702 to
be0aa3c
Compare
etrepum
force-pushed
the
claude/elementnode-replace-audit-ahvm8e
branch
from
August 2, 2026 04:27
be0aa3c to
7ecc2f4
Compare
etrepum
force-pushed
the
claude/elementnode-replace-audit-ahvm8e
branch
from
August 2, 2026 04:30
7ecc2f4 to
438c50d
Compare
etrepum
force-pushed
the
claude/elementnode-replace-audit-ahvm8e
branch
from
August 2, 2026 04:33
438c50d to
da12bff
Compare
…alues in the remaining block converters and diagnose replace() on a slot value ## Description Follow-up to facebook#8901 (fix for facebook#8894), which made $setBlocksType skip named slot values. The same crash class exists in every other selection-driven block converter that resolves the current block and replaces it through the tree API, and a direct replace() of a slot value still fails with the unhelpful generic "Expected node %s to have a parent" invariant. This audits and fixes the rest, applying the same rule: a slot value's assignment is managed by the node or extension that owns the slot, so generic converters leave it as-is. The eligibility checks test the slot up-link ($getSlotHost) rather than parentless-ness, so a detached node is never conflated with a slot value and keeps failing loudly. - lexical: LexicalNode.replace() on a slot value now throws a slot-specific invariant before any mutation ("use $setSlot on its host to assign a replacement"), mirroring the existing $removeFromParent guard for remove() on a slotted node. - @lexical/selection: the deprecated $wrapNodes no-ops when the selection lives in a bare-block slot frame (it previously threw in the sibling walk); a shadow-root slot value still wraps its own subtree normally. - @lexical/list: $insertList skips slot values in its empty-block branch, its empty-list-item branch, and its walk-up list-type-change branch; $removeList skips a slotted top-level list (they previously threw or vacated the slot). - @lexical/markdown: audited, already safe — the element/multiline shortcut runners bail unless the block's parent is a root or shadow root, which excludes slot values. The rule is documented in the named-slots concepts page. ## Test plan ### Before $insertList, $removeList, and $wrapNodes threw "Expected node %s to have a parent" (or silently vacated a slot) when the selection was inside a bare-block named-slot value or a slotted list; replace() on a slot value threw the same generic invariant. ### After The converters leave slot values as-is and replace() on a slot value throws a diagnostic pointing at $setSlot. New unit tests in LexicalSetBlocksTypeSlots.test.ts and LexicalFormatListSlots.test.ts, plus a replace() test in LexicalSlot.test.ts.
etrepum
force-pushed
the
claude/elementnode-replace-audit-ahvm8e
branch
from
August 2, 2026 04:36
da12bff to
4ad0642
Compare
…ion boundary ## Description Backspace with the caret at the boundary after a slot-bearing DecoratorNode host (e.g. the playground PullQuote) was a silent no-op: deleteCharacter special-cased slot-bearing decorators to never be removed via backspace, leaving them undeletable from the keyboard. Remove that special case so a slot-bearing decorator behaves like any other block decorator at the boundary: backspace removes it as a whole unit (slots included), and from an adjacent empty paragraph it drops the paragraph and node-selects the host so the next backspace deletes it. The ElementNode-host merge guard is intentionally unchanged: a cross-block merge would keep the host's children while silently dropping its slots, so backspace at the start of a slot-bearing host's child still leaves the host in place. Closes facebook#8904 ## Test plan ### Before With the caret at the start of the block after a slot-bearing decorator host, Backspace did nothing; the host could not be deleted from the keyboard. ### After Backspace removes the host (or node-selects it from an adjacent empty paragraph, matching normal block decorator behavior). New unit tests in LexicalSlot.test.ts cover both cases; the existing test that a slot-bearing ElementNode host is not merged away still passes.
Contributor
|
Ship-worthy. ✅ Checked out the PR, ran the tests (136/136 pass, incl. the 101-test ✅ What's right (verified)
🟡 Minor (non-blocking)
🔵 Nits (cosmetic)
🔎 Uncaught cases considered — all OK
|
potatowagon
approved these changes
Aug 3, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to no response for status checks
Aug 3, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to no response for status checks
Aug 3, 2026
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
Follow-up to #8901 (fix for #8894), which made
$setBlocksTypeskip named slot values. The same crash class exists in every other selection-driven block converter that resolves the current block and replaces it through the tree API, and a directreplace()of a slot value still fails with the unhelpful generic"Expected node %s to have a parent"invariant. This audits and fixes the rest, applying the same rule: a slot value's assignment is managed by the node or extension that owns the slot, so generic converters leave it as-is. The eligibility checks test the slot up-link ($getSlotHost) rather than parentless-ness, so a detached node is never conflated with a slot value and keeps failing loudly.lexical:LexicalNode.replace()on a slot value now throws a slot-specific invariant before any mutation, including the keys and types of the value and its host for debugging (replace: node %s (type %s) is slotted into host %s (type %s); … Use $setSlot on its host to assign a replacement.), mirroring the existing$removeFromParentguard forremove()on a slotted node.@lexical/selection: the deprecated$wrapNodesno-ops when the selection lives in a bare-block slot frame (it previously threw in the sibling walk); a shadow-root slot value still wraps its own subtree normally.@lexical/list:$insertListskips slot values in its empty-block branch, its empty-list-item branch, and its walk-up list-type-change branch;$removeListskips a slotted top-level list (they previously threw or vacated the slot).@lexical/markdown: audited, already safe — the element/multiline shortcut runners bail unless the block's parent is a root or shadow root, which excludes slot values.The rule is documented in the named-slots concepts page.
The audit also turned up the inverse problem: deleting a slot-bearing host from outside. Backspace with the caret at the boundary after a slot-bearing
DecoratorNodehost (e.g. the playground PullQuote) was a silent no-op —deleteCharacterspecial-cased slot-bearing decorators to never be removed via backspace, leaving them undeletable from the keyboard. That special case is removed, so a slot-bearing decorator behaves like any other block decorator at the boundary: backspace removes it as a whole unit (slots included), and from an adjacent empty paragraph it drops the paragraph and node-selects the host so the next backspace deletes it. The ElementNode-host merge guard is intentionally unchanged: a cross-block merge would keep the host's children while silently dropping its slots, so backspace at the start of a slot-bearing host's child still leaves the host in place.Closes #8904
Test plan
Before
$insertList,$removeList, and$wrapNodesthrew"Expected node %s to have a parent"(or silently vacated a slot) when the selection was inside a bare-block named-slot value or a slotted list;replace()on a slot value threw the same generic invariant. Backspace after a slot-bearing decorator host did nothing; the host could not be deleted from the keyboard.After
The converters leave slot values as-is and
replace()on a slot value throws a diagnostic pointing at$setSlot. Backspace removes a slot-bearing decorator host at the boundary (or node-selects it from an adjacent empty paragraph, matching normal block decorator behavior). New unit tests inLexicalSetBlocksTypeSlots.test.tsandLexicalFormatListSlots.test.ts, plusreplace()and boundary-deletion tests inLexicalSlot.test.ts; the existing test that a slot-bearing ElementNode host is not merged away still passes.