Skip to content

[lexical][lexical-rich-text][lexical-code-core] Bug Fix: Cursor stuck before leading inline DecoratorNode#8558

Merged
etrepum merged 1 commit into
facebook:mainfrom
mayrang:fix/8555-inline-decorator-move-to-end
May 26, 2026
Merged

[lexical][lexical-rich-text][lexical-code-core] Bug Fix: Cursor stuck before leading inline DecoratorNode#8558
etrepum merged 1 commit into
facebook:mainfrom
mayrang:fix/8555-inline-decorator-move-to-end

Conversation

@mayrang

@mayrang mayrang commented May 25, 2026

Copy link
Copy Markdown
Contributor

Description

When a paragraph (or heading) starts with an inline DecoratorNode and the caret is at element offset 0, pressing Cmd/Ctrl+ArrowRight leaves the caret stuck in front of the decorator. The Shift variant is also stuck — selection never extends past the decorator boundary. The symmetric backwards case is also broken in Chromium: with the caret inside the trailing text, Cmd/Ctrl+ArrowLeft stops at the decorator's right edge instead of reaching element offset 0. Safari handles the backwards case correctly.

Root cause: the decorator renders as contenteditable=false, so the native caret can't move past its boundary. And the Shift variants don't even reach the relevant lexical commands because isMoveToEnd / isMoveToStart matching masks omit shiftKey: 'any'.

What changed

  • packages/lexical/src/LexicalUtils.tsisMoveToEnd / isMoveToStart masks now include shiftKey: 'any', aligning them with isMoveForward / isMoveBackward. Shift variants now dispatch MOVE_TO_END / MOVE_TO_START instead of being dropped at the matcher.
  • packages/lexical-rich-text/src/index.ts — registers two COMMAND_PRIORITY_EDITOR listeners. MOVE_TO_END fires when anchor sits at element offset 0 and the block's first child is an inline DecoratorNode; it moves selection to the element end, with the Shift variant pinning anchor at element offset 0. MOVE_TO_START fires when the block's first child is an inline DecoratorNode and selection endpoints are within the same block; it moves focus to element offset 0, with the Shift variant preserving anchor. Cross-block selections fall through to native handling.
  • packages/lexical-code-core/src/CodeIndentation.ts$handleMoveTo now resolves the target via the existing framework helpers (selectNext / selectStart / setTextNodeRange / node.select), then restores the original anchor when event.shiftKey is set. Without this, the mask change would regress Shift+Cmd+Home/End inside code blocks to a collapsed caret.

Backwards compatibility

isMoveToEnd / isMoveToStart are exported. Any external listener registered on MOVE_TO_END / MOVE_TO_START will now also receive Shift events; collapse-only handlers may need to branch on event.shiftKey. Matches the pre-existing shape of isMoveForward / isMoveBackward, so the omission looks unintentional, not deliberate.

Known limitation

For a leading-inline-DecoratorNode paragraph that soft-wraps to a second visual line, the MOVE_TO_START handler unconditionally targets element offset 0 (block start) rather than the visual line start. Lexical state doesn't track visual lines, so the handler can't tell them apart. Out of scope for #8555 (single-line caret case); leaving as-is unless it surfaces in practice.

Open question

The non-Shift MOVE_TO_START caret lands as an element-type point (block, offset 0), because no text node exists before the inline decorator to anchor on. MOVE_TO_END non-Shift caret stays text-type via element.selectEnd() since the last descendant is a text node. The asymmetry is intentional — each shape is the natural representation of its target. If one type is preferred, MOVE_TO_END can be flipped to element-type to match.

Closes #8555

Test plan

  • pnpm test-unit -- packages/lexical packages/lexical-rich-text packages/lexical-code packages/lexical-code-core — all green (3231 passed, +10 new tests covering Shift / no-op / heading / mask matching / code-block regression).
  • pnpm tsc clean.
  • Manual on Chrome:
    • <inline equation><text> paragraph, caret at element offset 0 → Cmd+ArrowRight moves caret to text end; Cmd+Shift+ArrowRight selects equation + text.
    • Same paragraph, caret inside text → Cmd+ArrowLeft moves caret to element offset 0 (before equation); Cmd+Shift+ArrowLeft extends selection from text back to element offset 0.
    • Heading with same structure → same behaviors.
    • Two-paragraph mouse drag (P1 with leading inline decorator, P2 plain) → Cmd+(Shift+)ArrowLeft falls through to native, not overridden.
    • Code block, caret mid-line → Cmd+Shift+End / Shift+Home extend selection to line end / start.
  • Manual on Safari:
    • Same scenarios above — no regression. Cmd+ArrowLeft was already correct on Safari without this fix; the new handler produces the same result.

@vercel

vercel Bot commented May 25, 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 May 25, 2026 7:56am
lexical-playground Ready Ready Preview, Comment May 25, 2026 7:56am

Request Review

@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 May 25, 2026
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label May 25, 2026
@etrepum
etrepum added this pull request to the merge queue May 26, 2026
Merged via the queue into facebook:main with commit a1e1a38 May 26, 2026
45 checks passed
potatowagon added a commit that referenced this pull request May 28, 2026
PR #8558 broadened isMoveToStart/isMoveToEnd matchers to shiftKey:'any',
causing Shift+Cmd+Arrow to dispatch MOVE_TO_START/MOVE_TO_END. The handlers
call selectEnd()/selectStart() unconditionally, which throws when the element
contains only an inline DecoratorNode with no selectable text content.

This change:
1. Tightens isMoveToStart/isMoveToEnd back to Cmd+Arrow only (no shiftKey)
2. Adds defensive checks in MOVE_TO_END/MOVE_TO_START handlers to bail out
   (return false) when the element has no selectable text descendants

The original PR #8558 behavior for mixed-content (decorator + text) is preserved.
potatowagon added a commit that referenced this pull request May 28, 2026
PR #8558 broadened isMoveToStart/isMoveToEnd matchers to shiftKey:'any',
causing Shift+Cmd+Arrow to dispatch MOVE_TO_START/MOVE_TO_END. The handlers
call selectEnd()/selectStart() unconditionally, which throws when the element
contains only an inline DecoratorNode with no selectable text content.

This change:
1. Tightens isMoveToStart/isMoveToEnd back to Cmd+Arrow only (no shiftKey)
2. Adds defensive checks in MOVE_TO_END/MOVE_TO_START handlers to bail out
   (return false) when the element has no selectable text descendants

The original PR #8558 behavior for mixed-content (decorator + text) is preserved.
@etrepum etrepum mentioned this pull request May 28, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: The cursor doesn't move to the end of the line if the first node is a decorator

2 participants