Skip to content

Replace paragraph/heading options with configurable textBlocks - #6375

Closed
VPS-Andreas wants to merge 5 commits into
mainfrom
tiptap-text-block-config
Closed

VPS-Andreas wants to merge 5 commits into
mainfrom
tiptap-text-block-config

Conversation

@VPS-Andreas

@VPS-Andreas VPS-Andreas commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

The text block type select could only ever offer a fixed paragraph entry plus every enabled heading level, in a fixed order, with no way to show two distinct entries for the same underlying tag (e.g. a "Display" heading-1 variant next to a plain "Heading 1"). textBlockStyles' appliesTo-based style selection had the same limitation for styles.

textBlocks replaces the paragraph/heading options with a single ordered array of { name, tag, styles?, defaultStyle? } entries (plus label on the admin side), driving both the type dropdown's content and order directly. Multiple entries may share a tag; the stored textBlockName attribute (now mandatory on every paragraph/heading node) disambiguates them. textBlockStyles entries lose their appliesTo in favor of being referenced by name from textBlocks[].styles, and a configured defaultStyle is applied automatically and made mandatory (no "Default" option) for that entry.

defaultTextBlock names which textBlocks entry is the schema's default (for new/empty content, and for a heading-only schema, the ProseMirror default block), independent of the array's own dropdown order — restoring the independence the old heading.defaultLevel/heading.levels options had.

A safety-net migration (buildApplyTextBlocksMigration) always runs last, guaranteeing every node's textBlockName/textBlockStyle stay valid even after a later, textBlocks-unaware migration changes its tag (e.g. one that bumps every heading level) — and backfills textBlockName for legacy content that predates this attribute entirely.

listStyles is a new, separate option for a list item's text block style, since a list isn't a textBlocks entry of its own (it's toggled via a toolbar button, not chosen from the type dropdown) — previously expressed via appliesTo: ["ordered-list", "unordered-list"].

isTextBlockType (cms-admin only) marks a textBlockStyles entry as a text block's sole identity rather than a free style choice: as an entry's only style and defaultStyle, it hides that entry's otherwise pointless single-option style dropdown — covering the "Display" heading-1-variant case (#6369) without needing automatic dropdown-position heuristics, since order now comes directly from the textBlocks array position.

This still builds on TipTap's own Paragraph/Heading extensions (via .extend()), not a from-scratch replacement — only StarterKit's bundled copies are disabled in favor of the extended standalone packages, same as the existing pattern for textBlockStyle.

Example

createTipTapRichTextBlock({
    textBlocks: [
        { name: "paragraph", tag: "paragraph", label: "Paragraph", styles: ["copy100", "copy200"], defaultStyle: "copy100" },
        { name: "display", tag: "heading-1", label: "Display", styles: ["display100"], defaultStyle: "display100" },
        { name: "heading-1", tag: "heading-1", label: "Heading 1" },
        { name: "heading-2", tag: "heading-2", label: "Heading 2" },
    ],
    textBlockStyles: [
        { name: "copy100", label: "Copy 100", element: (props) => <p {...props} /> },
        { name: "copy200", label: "Copy 200", element: (props) => <p {...props} /> },
        { name: "display100", label: "Display", isTextBlockType: true, element: (props) => <h1 style={{ fontSize: 64 }} {...props} /> },
    ],
});

Also covered by createTipTapRichTextBlock.test.ts/.test.tsx, the migration tests, and the Storybook stories under "Heading" (Chromatic):

Open TODOs/questions

  • Add direct tests for buildApplyTextBlocksMigration (legacy heading content missing textBlockName entirely; a stale textBlockName after a later migration changes a shared-tag node's level)
  • Add a DraftJS-migration test where textBlockMap targets one of two textBlocks entries sharing a tag

🤖 Generated with Claude Code

VPS-Andreas and others added 3 commits September 16, 2026 09:15
The text block type select could only ever offer a fixed paragraph entry plus every enabled heading level, in a fixed order, with no way to show two distinct entries for the same underlying tag (e.g. a "Display" heading-1 variant next to a plain "Heading 1"). textBlockStyles' appliesTo-based style selection had the same limitation for styles.

textBlocks replaces the paragraph/heading options with a single ordered array of { name, tag, styles?, defaultStyle? } entries, driving both the type dropdown's content and order directly. Multiple entries may share a tag; the stored textBlockName attribute (now mandatory on every paragraph/heading node) disambiguates them. textBlockStyles entries lose their appliesTo in favor of being referenced by name from textBlocks[].styles, and a configured defaultStyle is applied automatically and made mandatory (no "Default" option) for that entry.

A safety-net migration (buildApplyTextBlocksMigration) always runs last, guaranteeing every node's textBlockName/textBlockStyle stay valid even after a later, textBlocks-unaware migration changes its tag (e.g. one that bumps every heading level).

listStyles is a new, separate option for a list item's text block style, since a list isn't a textBlocks entry of its own (it's toggled via a toolbar button, not chosen from the type dropdown) — previously expressed via appliesTo: ["ordered-list", "unordered-list"].

isTextBlockType (cms-admin only) marks a textBlockStyles entry as a text block's sole identity rather than a free style choice: as an entry's only style and defaultStyle, it hides that entry's otherwise pointless single-option style dropdown, matching how the legacy Draft.js RTE never separated element and style for a block type like this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adapts every existing story's config to textBlocks/listStyles, adds stories for the new defaultStyle-required style (no "Default" option) and isTextBlockType (a style acting as a text block's sole identity, e.g. a "Display" heading-1 variant), and splits the heading-focused stories (levels, heading-only, isTextBlockType) into their own file, matching the existing Translation/InlineStyles split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@VPS-Andreas VPS-Andreas self-assigned this Sep 16, 2026
VPS-Andreas and others added 2 commits September 16, 2026 10:08
textBlocks order was the only way to pick a schema's default block type (used for new/empty content and, for a heading-only schema, the ProseMirror default): whichever entry a consumer wanted as the default had to also be listed first in the type dropdown. The old paragraph/heading options didn't have this problem — heading's defaultLevel picked the default independently of the dropdown's own (unsorted, as-given) levels order.

defaultTextBlock restores that independence: an optional name of a textBlocks entry, defaulting to textBlocks[0] when omitted, that a consumer can set without disturbing the dropdown order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eading level via keyboard shortcut

In a heading-only schema, Mod-Alt-<level> bypassed the toolbar's type-switch logic and called setHeading directly, leaving textBlockName pointing at the old level's textBlocks entry and textBlockStyle unvalidated against the new level's styles. Extract the toolbar's "preserve style if still valid, else fall back to the target's default" logic into resolveTextBlockStyleForSwitch and reuse it from both the heading shortcut and the toolbar's dropdown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@VPS-Andreas

Copy link
Copy Markdown
Contributor Author

Closed in favor of #6382, #6383, #6384

@VPS-Andreas VPS-Andreas closed this Oct 6, 2026
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.

1 participant