Repository navigation
[lexical] Bug Fix: Keep the block cursor inside the DOMSlot before boundary - #9707
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
I kept this minimal by inserting the cursor before slot.before rather than appending to slot.element. Does that approach match what you want here, or would you prefer it go through a slot method? |
|
AI review status for this pull request.
|
There was a problem hiding this comment.
Thanks for the fix, this looks good. Inserting the block cursor before slot.before keeps it inside the slot's managed range. It also matches where ElementDOMSlot.getDecoratorBoundaryAnchor('trailing') already expects to find the cursor, and it leaves elements without a before boundary unchanged, since insertBefore(cursor, null) behaves like appendChild. The new test in LexicalDOMSlot.test.tsx fails without the change and passes with it.
On your question: using slot.before directly looks like the right choice to me. DOMSlot.insertChild anchors on getInsertionAnchor(), which falls back to the managed line break and the trailing decorator boundary anchor. Going through it would move the cursor ahead of that scaffolding even for elements with no before boundary.
Optional, non-blocking suggestions:
-
Guard against a
beforenode that has left the DOM (packages/lexical/src/LexicalUtils.ts:2285). AgetDOMSlotcan return a stored reference to its boundary, likeStoredTrailingDecorElementNodeinLexicalDOMSlot.test.tsx, and a native edit can remove that node.ElementNode.reconcileObservedMutationalready drops such a stale boundary (packages/lexical/src/nodes/LexicalElementNode.ts:1028-1034). In that state, with the caret after the last block decorator,insertBeforethrowsNotFoundErrorwhereappendChilddid not. Mirroring that guard avoids it:const before = slot.before !== null && slot.before.parentNode === elementDOM ? slot.before : null; elementDOM.insertBefore(blockCursorElement, before);
-
Add a changeset. CONTRIBUTING.md (Changesets) asks for one when a published package's DOM output changes, with a
Verify:paragraph for fixes that change output. For example,.changeset/lexical-block-cursor-slot-before.md:--- 'lexical': patch --- [fix] Keep the block cursor inside an element's DOM slot `before` boundary Verify: in an ElementNode whose `getDOMSlot` returns `withBefore(...)`, put the caret after its last block decorator; the block cursor renders before the trailing decoration instead of after it.
-
Paste the test output. AGENTS.md (Commits and Pull Requests) asks for the actual failing output under
### Beforeand the passing output under### Afterin the Test plan, rather than a description of them.
| elementDOM.appendChild(blockCursorElement); | ||
| // Stay inside the slot's managed range: a slot with a `before` | ||
| // boundary keeps its trailing decoration after the children. | ||
| elementDOM.insertBefore(blockCursorElement, slot.before); |
There was a problem hiding this comment.
Optional: if a getDOMSlot returns a stored before node that a native edit has removed, this insertBefore throws NotFoundError, which appendChild never did. ElementNode.reconcileObservedMutation already guards the same case (packages/lexical/src/nodes/LexicalElementNode.ts:1028-1034):
const before =
slot.before !== null && slot.before.parentNode === elementDOM
? slot.before
: null;
elementDOM.insertBefore(blockCursorElement, before);There was a problem hiding this comment.
Thanks for the fix, this looks good. Inserting the block cursor before slot.before keeps it inside the slot's managed range, matches where ElementDOMSlot.getDecoratorBoundaryAnchor('trailing') already looks for it, and leaves elements without a before boundary unchanged, since insertBefore(cursor, null) behaves like appendChild. The fallback for a before node that has left the element (packages/lexical/src/LexicalUtils.ts:2285-2289) mirrors the check in ElementNode.reconcileObservedMutation, and each of the two new tests in LexicalDOMSlot.test.tsx fails without its part of the change and passes with it.
On your question: using slot.before directly is the right choice. DOMSlot.insertChild anchors on getInsertionAnchor(), which for an element without a before boundary falls back to the trailing decorator boundary anchor (packages/lexical/src/LexicalDOMSlot.ts:324-330), so going through it would move the cursor ahead of that anchor for every element that ends in a block decorator. Its invariant (packages/lexical/src/LexicalDOMSlot.ts:162-165) would also throw in the detached-boundary case your guard now handles.
Optional, non-blocking:
- Paste the test output. AGENTS.md (Commits and Pull Requests) and
.github/pull_request_template.mdask the Test plan for the command you ran with its actual failing output under### Beforeand passing output under### After, rather than a description of the results.
lexical
@lexical/a11y
@lexical/clipboard
@lexical/code
@lexical/code-core
@lexical/code-prism
@lexical/code-shiki
@lexical/compiler
@lexical/devtools-core
@lexical/dragon
@lexical/eslint-plugin
@lexical/extension
@lexical/fast-check
@lexical/file
@lexical/hashtag
@lexical/headless
@lexical/history
@lexical/html
@lexical/internal
@lexical/link
@lexical/list
@lexical/mark
@lexical/markdown
@lexical/mdast
@lexical/offset
@lexical/overflow
@lexical/plain-text
@lexical/react
@lexical/rich-text
@lexical/selection
@lexical/table
@lexical/tailwind
@lexical/text
@lexical/utils
@lexical/y
@lexical/yjs
commit: |
Description
When a collapsed element selection sits after the last child of an element (for example after a block decorator) and that element's
getDOMSlotuseswithBefore,$updateDOMBlockCursorElementappended the block cursor to the end ofslot.element. That put the cursor after the slot's trailing decoration, outside the managed range.The cursor is now inserted before
slot.before(which isnullwhen there is no boundary, so behavior without a slot boundary is unchanged). This also matches whereElementDOMSlot.getDecoratorBoundaryAnchor('trailing')already expects to find the cursor (before.previousSibling).Fixes #9680
Test plan
LexicalDOMSlot.test.tsx(trailing decoration slot with a block decorator, selection at the end). It fails before the change and passes after.vitest --project unitforpackages/lexical,packages/lexical-rich-text,packages/lexical-selection: 663 files passed, 28582 tests passed.tsc --noEmit, eslint and prettier clean.