From fe95f9c720b5710b82353d9ad0a695a9733d1b36 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Mon, 3 Aug 2026 20:18:00 -0700 Subject: [PATCH] [lexical-list] Bug Fix: continue split-list numbering from the item's value, not its index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description With `registerList(editor, {restoreNumbering: true})`, pressing Enter on an empty list item splits the list and the tail list is given a `start` so that numbering continues where it left off. `$getNewListStart` computed that start as `list.getStart() + listItem.getIndexWithinParent()`. Index is not the same as the rendered number. `updateChildrenListItemValue` deliberately does not advance the counter for a list item that only wraps a nested list (`if (!$isListNode(child.getFirstChild())) value++`), because such an item renders no marker of its own. Every nested sublist before the split point therefore pushes the computed start one number too high, and the split-off list skips numbers: ``` 1. A • A.1 <- wrapper
  • , renders no number 2. B 3. 4. C ``` Before: the tail list starts at 4, so the document reads 1, 2, paragraph, 4, 5 — the number 3 disappears. Expected (and what the flat case already does): the tail list starts at 3. `ListItemNode.getValue()` is exactly the rendered number that `updateChildrenListItemValue` maintains, so using it fixes the nested case and is identical to the old expression for a flat list. ## Test plan ### Before New unit test in `LexicalListItemNode.test.ts` fails on `main`: ``` FAIL Option Enabled: Preserves numbering when a nested sublist precedes the split AssertionError: expected 4 to be 3 // Object.is equality - Expected + Received - 3 + 4 ``` ### After `pnpm exec vitest --project unit packages/lexical-list` passes (120 tests). --- .../unit/LexicalListItemNode.test.ts | 47 +++++++++++++++++++ packages/lexical-list/src/utils.ts | 6 ++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts b/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts index 058ef1c1bc3..f6382166886 100644 --- a/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts +++ b/packages/lexical-list/src/__tests__/unit/LexicalListItemNode.test.ts @@ -1503,6 +1503,53 @@ describe('LexicalListItemNode tests', () => { ); }); + test('Option Enabled: Preserves numbering when a nested sublist precedes the split', async () => { + const {editor} = testEnv; + await editor.update(() => { + const root = $getRoot(); + const list = $createListNode('number'); + const item1 = $createListItemNode(); + item1.append($createTextNode('A')); + + // A nested sublist lives inside its own wrapper
  • , which renders no + // marker of its own and so does not consume a number. + const nestedHolder = $createListItemNode(); + const nested = $createListNode('bullet'); + const nestedItem = $createListItemNode(); + nestedItem.append($createTextNode('A.1')); + nested.append(nestedItem); + nestedHolder.append(nested); + + const item2 = $createListItemNode(); + item2.append($createTextNode('B')); + const emptyItem = $createListItemNode(); + const item3 = $createListItemNode(); + item3.append($createTextNode('C')); + + list.append(item1, nestedHolder, item2, emptyItem, item3); + root.append(list); + + emptyItem.select(); + }); + + await editor.update(() => { + $handleListInsertParagraph(true); + }); + + editor.read('latest', () => { + // A is 1 and B is 2, so the removed empty item was 3 and the split-off + // list has to continue from 3 -- the same rule the flat case follows. + const [firstList, paragraph, secondList] = $getRoot().getChildren(); + expect($isListNode(firstList)).toBe(true); + expect($isParagraphNode(paragraph)).toBe(true); + expect($isListNode(secondList)).toBe(true); + expect((secondList as ListNode).getStart()).toBe(3); + const firstItem = (secondList as ListNode).getFirstChild(); + expect($isListItemNode(firstItem)).toBe(true); + expect((firstItem as ListItemNode).getValue()).toBe(3); + }); + }); + describe('ListItemNode $transform wraps orphan ListItemNodes', () => { test('wraps a single orphan ListItemNode under root in a ListNode', () => { const {editor} = testEnv; diff --git a/packages/lexical-list/src/utils.ts b/packages/lexical-list/src/utils.ts index b01befcba59..59e5a9979ae 100644 --- a/packages/lexical-list/src/utils.ts +++ b/packages/lexical-list/src/utils.ts @@ -201,5 +201,9 @@ export function $getNewListStart( list: ListNode, listItem: ListItemNode, ): number { - return list.getStart() + listItem.getIndexWithinParent(); + // The split-off list continues from the number the split point was rendered + // with. That is the item's value, not its index: `updateChildrenListItemValue` + // deliberately does not advance the counter for items that only wrap a nested + // list, so index and value diverge as soon as the list has a sublist. + return listItem.getValue(); }