[lexical-rich-text] Bug Fix: Insert paragraph on Enter for a block DecoratorNode NodeSelection#8526
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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 |
|
Thanks for the pointer. Looked at #8393 — the 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. |
|
Hmm, yeah, I think I could add a condition for NodeSelection in |
|
Thanks — I'll pick up the DecoratorNode + NodeSelection wiring once #8393 lands. |
| 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; | ||
| } |
There was a problem hiding this comment.
| 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
There was a problem hiding this comment.
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.
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.
Description
When a block-level
DecoratorNodeis selected as aNodeSelection(e.g. the user clicked on a YouTube embed), pressing Enter has no effect —KEY_ENTER_COMMANDregistered byregisterRichTextbails 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 viainsertAfter($createParagraphNode())and move the caret in. Inline DecoratorNodes and other NodeSelection shapes still fall through toreturn false, preserving current behaviour. RangeSelection / null selection paths are untouched.Closes #8525
Note
Decorators that don't go through
BlockWithAlignableContents/useLexicalNodeSelection(e.g.EquationNodein 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 (newRichTextNodeSelectionEnter.test.ts: 2 tests via$initialEditorStatefactory +buildEditorFromExtensions)pnpm tsc --noEmit -p tsconfig.jsoncleanpnpm flowcleannpx prettier --checkclean on changed files[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