Skip to content

feat(rich-text): preserve ordered list markers when unindenting out of a list - #1415

Merged
henningmu merged 2 commits into
mainfrom
preserve-ordered-list-marker-on-shift-tab
Jul 29, 2026
Merged

feat(rich-text): preserve ordered list markers when unindenting out of a list#1415
henningmu merged 2 commits into
mainfrom
preserve-ordered-list-marker-on-shift-tab

Conversation

@henningmu

@henningmu henningmu commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Overview

Typing a number followed by a dot and a space at the start of a paragraph (e.g. the German date 17. Juli) triggers the ordered-list input rule, which swallows the typed number into the list's start attribute. Shift-Tab to undo the list conversion, removed the number entirely (17. JuliJuli), with no way to recover it. This made it effectively impossible to start a line with such a date.

This PR adds a RichTextListItem extension (mirroring the RichTextOrderedList/RichTextBulletList pattern) that replaces the stock ListItem in RichTextKit. It only customizes Shift-Tab: when the lift takes the item out of an ordered list (as opposed to unindenting it into a parent list), the rendered marker is re-inserted as literal text at the start of the lifted paragraph — so Shift-Tab never destroys visible content.

Before After
17. Juli + Shift-TabJuli 17. Juli + Shift-Tab17. Juli

Implementation notes:

Bullet lists are intentionally left untouched: the number is user-meaningful data, a bullet is not.

Related: Doist/Issues#20622

PR Checklist

Test plan

  • Manually, in the preview Storybook:
    • Open the Rich-text → Default story
    • Type 17. Juli at the start of an empty line (converts to an ordered list starting at 17)
    • Press Shift-Tab
      • Observe the paragraph reads 17. Juli as plain text
    • Press Tab/Shift-Tab inside nested and bullet lists
      • Observe unchanged behavior everywhere else

…f a list

Typing a number followed by a dot and a space (e.g. the German date
`17. Juli`) converts the paragraph into an ordered list via the automatic
input rule, swallowing the typed number into the list's `start` attribute.
Unindenting the item with `Shift-Tab` then dropped the number entirely,
making it impossible to recover the original text.

`RichTextListItem` now extends the built-in `ListItem` extension so that
when `Shift-Tab` lifts an item out of an ordered list (rather than
unindenting it into a parent list), the rendered marker is re-inserted as
literal text at the start of the lifted paragraph, preserving exactly what
was on screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 14, 2026

Copy link
Copy Markdown

Deploy Preview for doist-typist ready!

Name Link
🔨 Latest commit 2e21d75
🔍 Latest deploy log https://app.netlify.com/projects/doist-typist/deploys/6a69a676d1292400084b6679
😎 Deploy Preview https://deploy-preview-1415--doist-typist.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@henningmu henningmu self-assigned this Jul 14, 2026
@henningmu
henningmu marked this pull request as ready for review July 14, 2026 12:20
@henningmu
henningmu requested a review from rfgamaral as a code owner July 14, 2026 12:20

@doistbot doistbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This PR adds a RichTextListItem extension that preserves ordered-list markers as literal text when Shift-Tab lifts an item out of a list — a nice fix for the date-at-start-of-line problem.

I also included a few optional follow-up notes in the details below.

Optional follow-up notes (3)
  • P3 src/extensions/rich-text/rich-text-list-item.ts:63: The editor.can().liftListItem(itemTypeName) guard at this line dry-runs the lift command's transaction logic to check feasibility, then editor.chain().liftListItem(itemTypeName) at line 75 runs the same logic again to actually apply it. The chain already handles failure — if liftListItem returns false, the chain returns false and the fallback this.editor.commands.liftListItem(this.name) on line 99 fires correctly. The guard adds no safety benefit (the lift happens before marker insertion, so there's no risk of partial execution) and doubles the lift computation on every Shift-Tab that reaches this path. Remove lines 63–65.
  • P3 src/extensions/rich-text/rich-text-list-item.test.ts:83: The describe block name says "when Shift-Tab does not lift the item out of a list," but the bullet-list test inside it (line 84) does lift the item out — it goes from <ul><li> to plain <p>. The describe name is only accurate for the nested ordered list test. Consider renaming to something like when marker preservation is not applicable or when Shift-Tab falls back to the default behavior.
  • P3 src/extensions/rich-text/rich-text-list-item.test.ts:47: The non-empty selection guard in liftOutOfOrderedListPreservingMarker (line 27 of rich-text-list-item.ts) is a deliberate safety behavior — it prevents ambiguous marker insertion when lifting a multi-item range — but no test covers it. Every test uses createEditorWithCaretAfter, which always creates a collapsed selection. A test that selects across two ordered-list items (e.g. via editor.commands.setTextSelection(from, to)) and asserts that Shift-Tab lifts without inserting a stray marker would protect against someone removing the guard. The existing harness makes this straightforward to add.

Share FeedbackReview Logs

Comment thread src/extensions/rich-text/rich-text-list-item.ts
Comment thread src/extensions/rich-text/rich-text-list-item.ts
Comment thread src/extensions/rich-text/rich-text-list-item.ts Outdated
Comment thread src/extensions/rich-text/rich-text-list-item.ts Outdated
Comment thread src/extensions/rich-text/rich-text-list-item.ts Outdated

@rfgamaral rfgamaral left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@henningmu Looks good to me, left a few comments that I think are worth addressing, but non-blocking.

@henningmu

Copy link
Copy Markdown
Contributor Author

Addressed both comments. Thanks, Ricardo 🙌

@henningmu
henningmu requested a review from rfgamaral July 29, 2026 07:30
@henningmu
henningmu merged commit 4408c72 into main Jul 29, 2026
11 checks passed
@henningmu
henningmu deleted the preserve-ordered-list-marker-on-shift-tab branch July 29, 2026 13:37
doist-release-bot Bot pushed a commit that referenced this pull request Jul 29, 2026
## [16.2.0](v16.1.0...v16.2.0) (2026-07-29)

### Features

* **rich-text:** preserve ordered list markers when unindenting out of a list ([#1415](#1415)) ([4408c72](4408c72))
@doist-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 16.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@doist-release-bot doist-release-bot Bot added the released Pull requests that have been released to production label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released Pull requests that have been released to production

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants