Skip to content

mail-react: Add a rich-text block factory for Tip-Tap content - #6406

Merged
VPS-Obi merged 6 commits into
mainfrom
mail-react/tip-tap-rich-text-block
Oct 7, 2026
Merged

VPS-Obi merged 6 commits into
mainfrom
mail-react/tip-tap-rich-text-block

Conversation

@VPS-Ricky

@VPS-Ricky VPS-Ricky commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

createTipTapRichTextBlock renders the CMS Tip-Tap rich text block in mails. It uses the same components as createRichTextBlock, so existing styling for the draft-js block also works here. It is a separate factory because the two store styling differently.

export const { MjmlTipTapRichTextBlock } = createTipTapRichTextBlock({
    textBlockStyles: {
        title: { variant: "title" },
        header: { variant: "header" },
    },
    textBlocks: {
        "unordered-list": { variant: "list" },
    },
});

In the admin, the content editor writes paragraphs and lists, and can give each one a style, such as title or header. The application defines these styles in the CMS block's textBlockStyles. The two mail options follow that:

  • textBlockStyles: the look of each entry in the rich text editor's style dropdown.
  • textBlocks: the default look of a paragraph or list, when a content editor did not select a style. Leave it out to use the theme's default variant.

Mails render all text as div elements, so a heading in a mail is a style, not a heading tag.

Screenshot

The same content in the demo Welcome Email, authored once in the draft-js block and once in the Tip-Tap block:

The Welcome Email rendering the same header, paragraphs and bullet list from the draft-js block and from the Tip-Tap block

Task: https://vivid-planet.atlassian.net/browse/PHSB2C-12548

@VPS-Ricky VPS-Ricky self-assigned this Sep 21, 2026
@VPS-Ricky
VPS-Ricky force-pushed the mail-react/tip-tap-rich-text-block branch 2 times, most recently from 82ff0df to 0277b7b Compare September 21, 2026 10:58
@nsams

nsams commented Sep 21, 2026

Copy link
Copy Markdown
Member

@VPS-Ricky
VPS-Ricky force-pushed the mail-react/tip-tap-rich-text-block branch 4 times, most recently from 38301b7 to 9853a7e Compare September 21, 2026 14:02
@VPS-Ricky

Copy link
Copy Markdown
Contributor Author

shouldn't we use a mechanism similar to site? https://github.com/vivid-planet/dextinity/blob/main/demo/site/src/common/blocks/TipTapRichTextBlock.tsx

It's the same mechanism as our existing draft-js block, createRichTextBlock.
So a factory, returning the Html and Mjml versions of the block (so the same configuration can be used in different contexts), which then render the text components that are optimized for mail-clients and render the variants from the configured theme.

Is that what you mean?

@VPS-Ricky
VPS-Ricky force-pushed the mail-react/tip-tap-rich-text-block branch 14 times, most recently from 621e327 to c9f32d9 Compare September 28, 2026 13:48
@VPS-Ricky
VPS-Ricky marked this pull request as ready for review September 28, 2026 13:53
@VPS-Ricky

Copy link
Copy Markdown
Contributor Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Adds new rich-text block type for email content.

The PR appears safe to merge based on this review.

Summary

The PR adds a Tip-Tap rich-text mail factory alongside the Draft.js factory and wires it into the demo’s admin, API, and mail rendering paths.

  • It reuses themed text and table-based list components, with configurable block styles, marks, and links.
  • It adds rendering tests, examples, documentation, and a changeset.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  CMS[CMS Tip-Tap content] --> Factory[createTipTapRichTextBlock]
  Factory --> Renderer[Tip-Tap content renderer]
  Renderer --> Text[Themed text blocks]
  Renderer --> Lists[Table-based lists]
  Renderer --> Inline[Marks and resolved links]
  Text --> Mail[HTML or MJML email]
  Lists --> Mail
  Inline --> Mail
Loading

Reviews (3) · Last reviewed commit: "Demo: Offer the Tip-Tap rich text block ..."

Comment thread packages/mail-react/src/blocks/richText/inlineElements.tsx
@nsams

nsams commented Sep 29, 2026

Copy link
Copy Markdown
Member

Is that what you mean?

....that is for "normal" sites, for mail-react:

  • you made a createRichTextBlock for the draftjs block

The question I'm asking is if we could have something similar to site's renderTipTapRichText for mail-react

@VPS-Ricky

Copy link
Copy Markdown
Contributor Author

The question I'm asking is if we could have something similar to site's renderTipTapRichText for mail-react

So you're asking if we should create a render-function instead of (or in addition to) the component/block factory, correct?

The reason for the factory is that we can control precisely how the text elements are rendered, like the specific table-structure we need for lists, so it renders consistently across mail-clients.

Font styling, responsive styling, spacings, etc. can all be controlled through the theme, registerStyles() and the factory’s options.

IMO replacing the factory with a render function would make it too easy to override the html structure with one that does not work consistently across mail-clients, while preventing the library from shipping fixes for specific mail-clients, if we discover new issues.

About adding the render-function in addition to the factory: it seems like one additional thing we'd need to support that will rarely to never be used in practice. Where it is used, we have the issue again, that fixes will not reach those projects.

@VPS-Ricky

Copy link
Copy Markdown
Contributor Author

@greptileai review

@VPS-Ricky

Copy link
Copy Markdown
Contributor Author

@greptileai

@VPS-Ricky
VPS-Ricky requested review from VPS-Obi and vps-manuel-blum and removed request for VPS-Obi and vps-manuel-blum October 1, 2026 11:09
@VPS-Obi

VPS-Obi commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

We discussed adding a render function when we added the Draft.js-based implementation (see #5873 (comment)). @VPS-Ricky convinced me that a factory is better because we need to support both MJML- and HTML-based rich text blocks, which would require two render function, which could drift apart.

Internally, the factory does little more than calling the render functions with correctly configured arguments:

export function createRichTextBlock<TLinkTypes extends Record<string, unknown> = Record<string, unknown>>(
options: CreateRichTextBlockOptions<TLinkTypes> = {},
): {
// The description below is duplicated in MjmlRichTextBlock.stories.tsx because Storybook cannot read TSDoc from factory return type properties. Update both when the description changes.
/** Renders CMS RichText block data (draft-js raw content) as one `MjmlText` per draft block. Must be placed within an `MjmlColumn`. */
MjmlRichTextBlock: (props: RichTextBlockProps) => ReactNode;
// The description below is duplicated in HtmlRichTextBlock.stories.tsx because Storybook cannot read TSDoc from factory return type properties. Update both when the description changes.
/** Renders CMS RichText block data (draft-js raw content) as one `HtmlText` div per draft block, for raw-HTML contexts such as `MjmlRaw`. Inside `MjmlRaw` in an `MjmlColumn`, place `HtmlRichTextBlock` in a `<tr>` and `<td>` of its own. */
HtmlRichTextBlock: (props: RichTextBlockProps) => ReactNode;
} {
const blockTypes = options.blockTypes ?? {};
const linkTypes = mergeLinkTypes<TLinkTypes>(options.linkTypes);
const inline = options.inline ?? {};
function MjmlRichTextBlock({ data }: RichTextBlockProps): ReactNode {
return renderRichTextContent({ draftContent: data.draftContent, blockTypes, linkTypes, inline, blockTextComponent: MjmlBlockText });
}
function HtmlRichTextBlock({ data }: RichTextBlockProps): ReactNode {
return renderRichTextContent({ draftContent: data.draftContent, blockTypes, linkTypes, inline, blockTextComponent: HtmlBlockText });
}
return { MjmlRichTextBlock, HtmlRichTextBlock };
}

IMO we can use the same approach for the TipTap-based implementation.

@VPS-Obi VPS-Obi changed the title Add a rich-text block factory for Tip-Tap content mail-react: Add a rich-text block factory for Tip-Tap content Oct 5, 2026
Comment thread demo/site/src/mail/blocks/smallListStyles.ts
@VPS-Ricky
VPS-Ricky requested a review from VPS-Obi October 5, 2026 08:45
VPS-Obi
VPS-Obi previously approved these changes Oct 5, 2026
jamesricky and others added 5 commits October 7, 2026 09:52
It is a separate factory, not an option of `createRichTextBlock`,
because the two store styling differently.
The top-level `textBlockStyles` option with `appliesTo` was replaced by
`styles` on each text block and list, so the mail blocks no longer
compiled against main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@VPS-Andreas

Copy link
Copy Markdown
Contributor

Rebased onto main while Ricky is away. #6383 was merged in the meantime and moved the TipTap block's text block styles from the top-level textBlockStyles option into styles on each text block and list. The demo mail blocks added in this PR still used the old option, so 8331de8 moves their styles over. The only other conflict was the migration table in the TipTap docs.

…eta.json

Main now writes a description for every TipTap block into
block-meta.json, so the file was out of date after the rebase.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@VPS-Obi
VPS-Obi merged commit aca736f into main Oct 7, 2026
13 of 16 checks passed
@VPS-Obi
VPS-Obi deleted the mail-react/tip-tap-rich-text-block branch October 7, 2026 08:38
VPS-Obi pushed a commit that referenced this pull request Oct 7, 2026
)

In the admin, a content editor can give a nested list item its own
style, but the mail rendered the item with the style of the item around
it. A nested list has no text component of its own, so its cells copied
the text styles of the enclosing item, and the responsive rules of the
outermost list reached the nested cells too.

A nested item without a style now gets its list's `textBlocks` style,
not the style of the item around it, because the admin shows it that way
too.

## Screenshots/screencasts

The `WithListVariants` story of `MjmlTipTapRichTextBlock`:

| Before | Now |
| ------ | --- |
| <img width="539" alt="nested-list-before-desktop"
src="https://github.com/user-attachments/assets/fca23e84-8513-42cf-b89e-0cdfa7187654"
/> | <img width="539" alt="nested-list-after-desktop"
src="https://github.com/user-attachments/assets/84d6d742-4dc0-4ee2-bb91-40e2034d94a6"
/> |

## Changeset

None needed: this fixes #6406, which is not released yet.

---

Task: https://vivid-planet.atlassian.net/browse/PHSB2C-12548

---------

Co-authored-by: Ricky Smith <jamesricky@me.com>
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.

5 participants