Skip to content

[lexical-list] Feature: Preserve ordered list numbering when split by blocks or paragraphs#8092

Merged
etrepum merged 2 commits intofacebook:mainfrom
Sa-Te:fix/list-numbering-interruption
Feb 6, 2026
Merged

[lexical-list] Feature: Preserve ordered list numbering when split by blocks or paragraphs#8092
etrepum merged 2 commits intofacebook:mainfrom
Sa-Te:fix/list-numbering-interruption

Conversation

@Sa-Te
Copy link
Copy Markdown
Contributor

@Sa-Te Sa-Te commented Jan 25, 2026

Summary

Fixes #8037

Currently, when an Ordered List is interrupted by a Block Node (like a Code Block) or split via the Enter key (creating a Paragraph), the subsequent list resets its numbering to 1.

This PR fixes the numbering continuity by:

  1. Updating Logic (LexicalListItemNode & formatList): Changed the list splitting logic to calculate the newStart index based on the splitting item's indexWithinParent rather than relying on fragile sibling counts.
  2. Updating Renderer (LexicalListNode): Added a check in updateDOM to ensure changes to the start attribute are actually applied to the DOM.

Test Plan

repro steps:

  1. Open Playground.
  2. Create an ordered list:
    1. Item A
    2. Item B
    3. Item C
  3. Place cursor at the end of "Item B" and press Enter (creating a new list item).
  4. Insert a Code Block (or press Enter again to create a Paragraph).

Expected Behavior (Fixed):
The list below the block should continue numbering at 3.

Actual Behavior (Before Fix):
The list below the block resets to 1.

Implementation Details

  • LexicalListNode.ts: Added prevNode.__start !== this.__start check in updateDOM.
  • LexicalListItemNode.ts: Updated replace and insertAfter to calculate newStart using getIndexWithinParent().
  • formatList.ts: Updated $handleListInsertParagraph to calculate newStart using getIndexWithinParent() when splitting a list with a paragraph.

@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 Jan 25, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Jan 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Feb 5, 2026 11:49am
lexical-playground Ready Ready Preview, Comment Feb 5, 2026 11:49am

Request Review

Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

Overall this looks like a good direction, but it can be done in less code, and there should be new tests to show that this new behavior works as expected.

Comment on lines +573 to +580
const listType = parent.getListType();
let newStart = 1;

if (listType === 'number') {
newStart = parent.getStart() + listItem.getIndexWithinParent();
}

const newList = $createListNode(parent.getListType(), newStart);
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.

Since this code is basically repeated 3 times maybe it should be consolidated into a function

Comment on lines 231 to 236
let nextSibling = this.getNextSibling();
const nextSiblings = [];
while (nextSibling) {
const nodeToAppend = nextSibling;
nextSiblings.push(nextSibling);
nextSibling = nextSibling.getNextSibling();
newList.append(nodeToAppend);
}
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.

This is a long way of writing

const nextSiblings = this.getNextSiblings();

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.

Thanks for the guidance! Its a real honor to learn from a mentor like you.

I've refactored the code to use getNextSiblings() and consolidated the start index calculation into a $getNewListStart helper in utils.ts to reduce duplication. I also added a regression test in LexicalListItemNode.test.ts to verify numbering continuity when splitting lists.

@levensta
Copy link
Copy Markdown
Contributor

Hello, @Sa-Te. If this becomes the default behavior, how can I reset the counter for the split list section? I'd also like to be able to configure this as an option. I think that for some users this change will come as a surprise and will require additional redesign of those services that are integrated with lexical-based editors

@Sa-Te
Copy link
Copy Markdown
Contributor Author

Sa-Te commented Jan 26, 2026

Hello, @Sa-Te. If this becomes the default behavior, how can I reset the counter for the split list section? I'd also like to be able to configure this as an option. I think that for some users this change will come as a surprise and will require additional redesign of those services that are integrated with lexical-based editors

Cheers for the feedback, mate!
The main idea here was to sort out the current behaviour, which feels a bit wonky compared to standard kit like Google Docs or Word. Most users get a bit miffed when their list resets to 1 automatically just because they popped a paragraph in the middle.

As for resetting the counter manually, that would likely be handled by the UI (like a 'Restart Numbering' context menu) manipulating the 'start' attribute, rather than relying on the default split behaviour.

I’m happy to leave it to the maintainers to decide if this needs to be behind a configuration flag, but personally, I reckon bringing it in line with standard rich-text expectations is the way forward.

@levensta
Copy link
Copy Markdown
Contributor

The main idea here was to sort out the current behaviour, which feels a bit wonky compared to standard kit like Google Docs or Word. Most users get a bit miffed when their list resets to 1 automatically just because they popped a paragraph in the middle.

As for resetting the counter manually, that would likely be handled by the UI (like a 'Restart Numbering' context menu) manipulating the 'start' attribute, rather than relying on the default split behaviour.

I’m happy to leave it to the maintainers to decide if this needs to be behind a configuration flag, but personally, I reckon bringing it in line with standard rich-text expectations is the way forward.

Google Docs and Word really don't reset the counter, and I understand the motivation to bring the behavior in line with popular editors. But this is only one of the ways a list can be split. For example, Notion, just like lexical in its current state, splits list into separate nodes when you try to insert a paragraph between items.

I'm not opposed to your change, but if you made it possible to reset the new default behavior, it would make the feature truly excellent

Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

What do you think about adding an option to the list extension that defaults to the previous behavior, but can be configured to have this new behavior? I think that would solve the compatibility issue with people that want to preserve the existing behavior

@Sa-Te
Copy link
Copy Markdown
Contributor Author

Sa-Te commented Feb 3, 2026

What do you think about adding an option to the list extension that defaults to the previous behavior, but can be configured to have this new behavior? I think that would solve the compatibility issue with people that want to preserve the existing behavior

Thanks @etrepum and @levensta for the feedback! I've refactored the approach to be fully opt-in and backward compatible, moving the logic from the Node level to the Command level.

Changes in this update:
Reverted LexicalListItemNode: The low-level insertAfter / replace methods now behave exactly as before (resetting numbering to 1 on split). This ensures no breaking changes for existing integrations.

Configurable Logic: Updated $handleListInsertParagraph (and registerList) to accept a shouldPreserveNumbering option.
false (Default): Resets new list to 1.
true (Opt-in): Calculates the correct continuity (e.g., 1, 2, [split], 3).

React Plugin: Updated to accept a shouldPreserveNumbering prop.

Playground: Enabled this feature by default in the Playground so the "smart" behavior is visible there.

Tests: Added unit tests covering both scenarios:
Verifying that default usage resets numbering (Legacy).
Verifying that { shouldPreserveNumbering: true } correctly maintains continuity.

Note: I have disabled the feature in the Playground Editor.tsx by default to ensure existing E2E tests pass. The logic is verified via Unit Tests.

Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

I'll give this another review when the build passes, there seem to be a lot of new errors

@Sa-Te Sa-Te force-pushed the fix/list-numbering-interruption branch from 3715316 to 5e9cc8d Compare February 4, 2026 23:06
@etrepum etrepum changed the title Fix(list): Preserve ordered list numbering when split by blocks or paragraphs [lexical-list] Feature: Preserve ordered list numbering when split by blocks or paragraphs Feb 5, 2026
Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

This is missing support in ListExtension, the configuration should also be in its ListConfig.

Comment on lines +79 to +81
export type RegisterListOptions = {
restoreNumbering?: boolean;
};
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
export type RegisterListOptions = {
restoreNumbering?: boolean;
};
export interface RegisterListOptions {
restoreNumbering?: boolean;
}

Comment on lines +211 to +212
const children = list.getChildren();
const index = children.indexOf(listItem);
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 children = list.getChildren();
const index = children.indexOf(listItem);
const index = listItem.getIndexWithinParent();

@etrepum etrepum added this pull request to the merge queue Feb 6, 2026
Merged via the queue into facebook:main with commit 2ccc73a Feb 6, 2026
39 checks passed
rayterion pushed a commit to rayterion/lexical that referenced this pull request Feb 8, 2026
@etrepum etrepum mentioned this pull request Feb 25, 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: 有序列表中间插入code block,序号被打断了

3 participants