From cf7b881fdf1efd6deb082cdc10cbab82fcd77d01 Mon Sep 17 00:00:00 2001 From: mayrang Date: Tue, 19 May 2026 12:49:14 +0900 Subject: [PATCH] [lexical-rich-text] Bug Fix: Insert paragraph on Enter for a block DecoratorNode NodeSelection (#8526) --- .../unit/RichTextNodeSelectionEnter.test.ts | 70 +++++++++++++++++++ packages/lexical-rich-text/src/index.ts | 19 ++++- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/lexical-rich-text/src/__tests__/unit/RichTextNodeSelectionEnter.test.ts diff --git a/packages/lexical-rich-text/src/__tests__/unit/RichTextNodeSelectionEnter.test.ts b/packages/lexical-rich-text/src/__tests__/unit/RichTextNodeSelectionEnter.test.ts new file mode 100644 index 00000000000..92cb71c1444 --- /dev/null +++ b/packages/lexical-rich-text/src/__tests__/unit/RichTextNodeSelectionEnter.test.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {buildEditorFromExtensions} from '@lexical/extension'; +import {RichTextExtension} from '@lexical/rich-text'; +import { + $createNodeSelection, + $getRoot, + $getSelection, + $isRangeSelection, + $setSelection, + KEY_ENTER_COMMAND, + ParagraphNode, +} from 'lexical'; +import { + $createTestDecoratorNode, + TestDecoratorNode, +} from 'lexical/src/__tests__/utils'; +import {assert, describe, expect, test} from 'vitest'; + +function createEditor(inline: boolean) { + return buildEditorFromExtensions({ + $initialEditorState: () => { + const decorator = $createTestDecoratorNode().setIsInline(inline); + $getRoot().append(decorator); + const selection = $createNodeSelection(); + selection.add(decorator.getKey()); + $setSelection(selection); + }, + dependencies: [RichTextExtension], + name: 'test', + nodes: [TestDecoratorNode], + }); +} + +describe('KEY_ENTER_COMMAND on a single-decorator NodeSelection', () => { + test('block decorator: inserts a paragraph after it and moves the caret', () => { + using editor = createEditor(false); + + editor.dispatchCommand(KEY_ENTER_COMMAND, null); + + editor.read(() => { + const root = $getRoot(); + expect(root.getChildrenSize()).toBe(2); + const children = root.getChildren(); + expect(children[0]).toBeInstanceOf(TestDecoratorNode); + expect(children[1]).toBeInstanceOf(ParagraphNode); + const selection = $getSelection(); + assert($isRangeSelection(selection)); + expect(selection.anchor.key).toBe(children[1].getKey()); + }); + }); + + test('inline decorator: no-op', () => { + using editor = createEditor(true); + + editor.dispatchCommand(KEY_ENTER_COMMAND, null); + + editor.read(() => { + const root = $getRoot(); + expect(root.getChildrenSize()).toBe(1); + expect(root.getChildren()[0]).toBeInstanceOf(TestDecoratorNode); + }); + }); +}); diff --git a/packages/lexical-rich-text/src/index.ts b/packages/lexical-rich-text/src/index.ts index 91152163421..a795a350aa9 100644 --- a/packages/lexical-rich-text/src/index.ts +++ b/packages/lexical-rich-text/src/index.ts @@ -1119,7 +1119,24 @@ export function registerRichText( editor.registerCommand( KEY_ENTER_COMMAND, event => { - const selection = $getSelection(); + 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. + // Convert to a RangeSelection past the decorator so the default + // RangeSelection handler below inserts a paragraph and places + // the caret. + if ($isNodeSelection(selection)) { + const nodes = selection.getNodes(); + if ( + nodes.length === 1 && + $isDecoratorNode(nodes[0]) && + !nodes[0].isInline() + ) { + selection = nodes[0].selectNext(); + } + } if (!$isRangeSelection(selection)) { return false; }