Skip to content

Fix saving a TipTap rich text that ends with a list - #6520

Merged
VPS-Obi merged 5 commits into
mainfrom
fix-tip-tap-trailing-empty-list
Oct 7, 2026
Merged

VPS-Obi merged 5 commits into
mainfrom
fix-tip-tap-trailing-empty-list

Conversation

@SebiVPS

@SebiVPS SebiVPS commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Since 10.8.0 (#6382), a TipTap rich text that ends with a list got an empty bulletList appended by the editor. A list without items is invalid, so the API rejected the content on save with "Validation failed". Deleting the list did not help: the empty list stayed in the content, rendered as an empty <ul></ul>, and editors could not see, select or remove it until they reloaded. Selecting all content and deleting it was affected too. It produced a bulletList with a listItem and a text block instead of one empty text block, and in a block that only allows headings it produced a horizontalRule.

ProseMirror needs a default block type in some places, and no setting defines it: it is the first node of the block group in schema order (schema.topNodeType.contentMatch.defaultType). TipTap's TrailingNode inserts it after content that ends with a list or a child block, and ProseMirror's createAndFill fills an emptied document with it. The schema order follows the extensions' priority. TipTap's Paragraph has priority 1000, so the paragraph used to be the default block type. #6382 replaced the paragraph and heading nodes with the textBlock node, which had no priority and is registered after StarterKit's lists, so bulletList became the default block type.

Solution

textBlock now has priority: 1000 in the Admin and in the API (the API schema must match the Admin schema). This restores what #6382 removed: textBlock takes the paragraph's place in the schema, so every place that uses the default block type gets a text block again, not only TrailingNode. The editor adds an empty text block after a trailing list, as it did before 10.8.0. Other approaches only fix part of the problem:

  • Setting the node option of TrailingNode to textBlock fixes TrailingNode only. An emptied document would still become a list.
  • Without TrailingNode, nothing follows a child block at the end of the content, so editors could not type after it.
  • Removing empty lists in the API hides the symptom. The editor would still create invalid content.
  • Registering textBlock before StarterKit gives the same schema order, but only as long as nobody changes the order of the extensions. priority is TipTap's way to define it, with the same value as TipTap's Paragraph.

With maxTextBlocks, this empty text block must not be added when the content is already at the limit. Otherwise a list as the last allowed block is followed by a block over the limit, and the API rejects the content. 10.8.0 had the same problem for ordered lists. It only avoided it for bullet lists by accident, because TrailingNode never adds a block after its own default type. The maxTextBlocks extension now sets TipTap's skipTrailingNodeMeta on transactions that leave the content at or over the limit. A filter that rejects these transactions would also reject pastes over the limit instead of cutting them off.

@tiptap/extensions exports skipTrailingNodeMeta, so it is now a direct dependency of @dextinity/cms-admin. StarterKit already depends on it.

The "List Text Block Styles" story clicked the editor to put the caret at the end of the list item. With the empty text block after the list, that click lands below the list, so the story now clicks the list item text.

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Fixes rich text editor behavior when content ends with a list.

The PR appears safe to merge; no new actionable issue was established.

Summary

The PR restores textBlock as TipTap’s default block type in the Admin and API, so a rich text ending in a list no longer gains an invalid empty list.

  • It suppresses the trailing text block when adding one would exceed maxTextBlocks.
  • It updates tests, story interactions, and the package dependency used by that suppression.

Reviews (2) · Last reviewed commit: "cms-admin: Don't add the trailing empty ..." · Reviewed by Greptile

Comment thread packages/admin/cms-admin/src/blocks/tipTap/extensions/TextBlock.tsx
@SebiVPS
SebiVPS force-pushed the fix-tip-tap-trailing-empty-list branch from 0120b9c to e1b51a1 Compare October 7, 2026 05:41
@SebiVPS
SebiVPS marked this pull request as draft October 7, 2026 05:44
@SebiVPS
SebiVPS marked this pull request as ready for review October 7, 2026 06:38
@SebiVPS
SebiVPS requested a review from VPS-Obi October 7, 2026 06:38
@SebiVPS

This comment was marked as resolved.

@SebiVPS
SebiVPS force-pushed the fix-tip-tap-trailing-empty-list branch from 873e8aa to 9f3aa03 Compare October 7, 2026 06:59
SebiVPS and others added 4 commits October 7, 2026 09:22
Since 10.8.0, the editor added an empty bulletList after a list at the end
of the content. The API rejects a list without items, so saving failed.
Removing the list didn't fix it: the empty list stayed at the end of the
content, where editors couldn't see or remove it.

ProseMirror's default block type is the schema's first block node.
TrailingNode inserts it after content that doesn't end with a text block,
and an emptied document is filled with it. The textBlock node, which
replaced the paragraph and heading nodes, had no priority, so it was
registered after StarterKit's lists, and the default block type became
bulletList (or horizontalRule in a heading-only block).

The textBlock node gets the paragraph's priority, in the Admin and in the
API, whose schema has to match.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The story clicked the editor to place the caret at the end of the list
item. Now that TrailingNode adds an empty text block after a list again,
that click can place the caret in the text block instead, so the nested
bullet list was created outside of the ordered list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the text block as the default block type, TrailingNode adds an
empty text block after a list at the end of the content. When the list
is the last block maxTextBlocks allows, that block exceeds the limit, and
the limit handler can't remove it for good, because TrailingNode adds it
again. The content then can't be saved.

Transactions that leave the content at or over the limit now skip
TrailingNode.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@SebiVPS
SebiVPS force-pushed the fix-tip-tap-trailing-empty-list branch from 9f3aa03 to f915a41 Compare October 7, 2026 07:22
VPS-Obi
VPS-Obi previously approved these changes Oct 7, 2026
@VPS-Obi

VPS-Obi commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@SebiVPS please resolve conflicts.

…empty-list

# Conflicts:
#	packages/admin/cms-admin/src/blocks/tipTap/createTipTapRichTextBlock.test.tsx
@VPS-Obi
VPS-Obi merged commit e9067c3 into main Oct 7, 2026
17 of 23 checks passed
@VPS-Obi
VPS-Obi deleted the fix-tip-tap-trailing-empty-list branch October 7, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants