Skip to content

[lexical-rich-text] Bug Fix: Insert paragraph on Enter for a block DecoratorNode NodeSelection#8526

Merged
etrepum merged 2 commits into
facebook:mainfrom
mayrang:fix/decorator-only-root-enter
May 19, 2026
Merged

[lexical-rich-text] Bug Fix: Insert paragraph on Enter for a block DecoratorNode NodeSelection#8526
etrepum merged 2 commits into
facebook:mainfrom
mayrang:fix/decorator-only-root-enter

Conversation

@mayrang

@mayrang mayrang commented May 18, 2026

Copy link
Copy Markdown
Contributor

Description

When a block-level DecoratorNode is selected as a NodeSelection (e.g. the user clicked on a YouTube embed), pressing Enter has no effect — KEY_ENTER_COMMAND registered by registerRichText bails out at the $isRangeSelection(selection) guard. The most visible case is when the decorator is the only root child (after the user removed all surrounding paragraphs), where the editor becomes uneditable from the keyboard, but the same no-op fires for any block-decorator NodeSelection — e.g. between paragraphs in the middle of the document.

Fix

Add a NodeSelection branch in the rich-text Enter handler: if the selection holds a single block DecoratorNode (!isInline()), insert a new empty paragraph after the decorator via insertAfter($createParagraphNode()) and move the caret in. Inline DecoratorNodes and other NodeSelection shapes still fall through to return false, preserving current behaviour. RangeSelection / null selection paths are untouched.

Closes #8525

Note

Decorators that don't go through BlockWithAlignableContents / useLexicalNodeSelection (e.g. EquationNode in the playground manages selection inside its React component without producing a lexical NodeSelection) aren't covered here — the rich-text Enter handler never sees them. That's a separate node-side fix.

Test plan

  • pnpm vitest run --project unit packages/lexical-rich-text — 5 files / 53 tests pass (new RichTextNodeSelectionEnter.test.ts: 2 tests via $initialEditorState factory + buildEditorFromExtensions)
  • pnpm tsc --noEmit -p tsconfig.json clean
  • pnpm flow clean
  • npx prettier --check clean on changed files
  • Manual playground:
    • [YouTube] only root state → Enter → empty paragraph appended, caret moves in
    • [paragraph, YouTube, paragraph] → YouTube NodeSelection + Enter → new empty paragraph between YouTube and next paragraph
    • Regular RangeSelection + Enter → unchanged (paragraph split)
    • Inline DecoratorNode NodeSelection + Enter → unchanged

@vercel

vercel Bot commented May 18, 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 18, 2026 11:15pm
lexical-playground Ready Ready Preview, Comment May 18, 2026 11:15pm

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 18, 2026
@levensta

Copy link
Copy Markdown
Contributor

It might make sense to also handle arrow keys if selection is set on a decorator that is an first/last node in the document. I do something similar here but for regular ElementNode #8393

@mayrang

mayrang commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the pointer. Looked at #8393 — the $onEscapeUp/Down helpers there take an ElementNode type guard and only act on $isRangeSelection collapsed at offset 0 / end, so the DecoratorNode NodeSelection case this PR handles doesn't reach them. Wiring them to also cover a single block decorator selected as a NodeSelection (or letting the predicate accept LexicalNode and branching on selection type) would line up nicely — both PRs would together cover the full keyboard escape, no duplication.

This PR will keep its Enter scope for now. If you're open to extending the helpers in #8393, I'll follow up with a PR wiring DecoratorNode + NodeSelection through them once #8393 lands.

@levensta

Copy link
Copy Markdown
Contributor

Hmm, yeah, I think I could add a condition for NodeSelection in $onEscapeUp/Down. I'll get back to that once I figure out the Firefox issues in my PR

@mayrang

mayrang commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — I'll pick up the DecoratorNode + NodeSelection wiring once #8393 lands.

Comment thread packages/lexical-rich-text/src/index.ts Outdated
Comment on lines +1122 to +1145
const selection = $getSelection();
// When a block-level DecoratorNode is selected as a NodeSelection
// (e.g. it is the only root child after the user removed all
// surrounding paragraphs), Enter has no RangeSelection to act on
// and the default handler bails out, leaving the editor stuck.
// Insert an empty paragraph after the decorator and place the
// caret in it so the user can keep typing.
if ($isNodeSelection(selection)) {
const nodes = selection.getNodes();
if (
nodes.length === 1 &&
$isDecoratorNode(nodes[0]) &&
!nodes[0].isInline()
) {
const newParagraph = $createParagraphNode();
nodes[0].insertAfter(newParagraph);
newParagraph.select();
if (event !== null) {
event.preventDefault();
}
return true;
}
return false;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const selection = $getSelection();
// When a block-level DecoratorNode is selected as a NodeSelection
// (e.g. it is the only root child after the user removed all
// surrounding paragraphs), Enter has no RangeSelection to act on
// and the default handler bails out, leaving the editor stuck.
// Insert an empty paragraph after the decorator and place the
// caret in it so the user can keep typing.
if ($isNodeSelection(selection)) {
const nodes = selection.getNodes();
if (
nodes.length === 1 &&
$isDecoratorNode(nodes[0]) &&
!nodes[0].isInline()
) {
const newParagraph = $createParagraphNode();
nodes[0].insertAfter(newParagraph);
newParagraph.select();
if (event !== null) {
event.preventDefault();
}
return true;
}
return false;
}
let selection = $getSelection();
// When a block-level DecoratorNode is selected as a NodeSelection
// (e.g. it is the only root child after the user removed all
// surrounding paragraphs), Enter has no RangeSelection to act on
// and the default handler bails out, leaving the editor stuck.
// Insert an empty paragraph after the decorator and place the
// caret in it so the user can keep typing.
if ($isNodeSelection(selection)) {
const nodes = selection.getNodes();
if (
nodes.length === 1 &&
$isDecoratorNode(nodes[0]) &&
!nodes[0].isInline()
) {
selection = nodes[0].selectNext();
}
}

Can re-use more code if we just change the selection to a RangeSelection

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — selection = nodes[0].selectNext() converts the NodeSelection past the decorator. The existing RangeSelection branch below handles paragraph insertion and caret. Unit tests (53/53) pass.

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label May 18, 2026
Per etrepum's review on PR facebook#8526: instead of handling the NodeSelection
case with a separate $createParagraphNode + insertAfter + select + return
path, convert the selection past the decorator via `selectNext()` and let
the existing RangeSelection branch below handle the paragraph insertion
and caret placement. selection is now `let` to allow the reassignment.
Same end behavior, less code, single path through.
@etrepum etrepum added this pull request to the merge queue May 19, 2026
Merged via the queue into facebook:main with commit cf7b881 May 19, 2026
73 checks passed
@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: Editor becomes uneditable when a block DecoratorNode is the only root child

3 participants