Skip to content

fix(RichTextEditor, ScrollArea): wrap long words and URLs instead of scrolling horizontally#2781

Open
sgruetter wants to merge 4 commits into
mainfrom
fix/rte-comment-long-word-wrap
Open

fix(RichTextEditor, ScrollArea): wrap long words and URLs instead of scrolling horizontally#2781
sgruetter wants to merge 4 commits into
mainfrom
fix/rte-comment-long-word-wrap

Conversation

@sgruetter

@sgruetter sgruetter commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Intent

Long unbreakable strings — URLs, long words — in rich text no longer force a sideways scroll. They
overflowed horizontally instead of wrapping, most visibly in the web-app comment sidebar both while
typing and after posting (a WCAG 1.4.10 Reflow failure at narrow widths). This fixes the wrapping in
RichTextEditor.

The issue is shown here:

scrolling.mov

Root cause

The editor and the serialized rendition already carried overflow-wrap: break-word, but break-word
only wraps visually — it does not lower the element's reported min-content width. Inside a
container that sizes to its content the width therefore still grows to the longest token, and the
text never wraps. The comment sidebar is exactly such a container: ScrollArea's Radix viewport
wraps its children in an inline display: table; min-width: 100% element that shrink-wraps to
max-content. List items compound it by rendering as inline-flex, which also shrink-wraps.

overflow-wrap: anywhere wraps like break-word visually and lowers the reported min-content
width, so the content wraps to the available width even inside a table / inline-flex context.

What changed

  • RichTextEditor — paragraphs and list items, both while editing and in the serialized (posted)
    rendition, now use overflow-wrap: anywhere instead of break-word.
  • The earlier ScrollArea display: block !important override is reverted. The defect lives in
    RTE, and forcing block on the shared Radix viewport (~66 web-app consumers) was more blast radius
    than the fix needs — overflow-wrap: anywhere fixes the content regardless of its container.
  • The tw-max-w-full cap on list items is dropped — redundant once min-content is lowered.

Genuinely wide, unshrinkable content (fixed-width media, nowrap) still scrolls horizontally.

Evidence

  • New RichTextEditor component test (real browser, LongContentWrapping.cy.tsx) drives a long
    unbreakable token through a faithful reproduction of Radix's display: table; min-width: 100%
    viewport — for the editor paragraph, the editor list item, and the serialized rendition — and
    asserts no horizontal overflow (scrollWidth ≤ clientWidth). It is red on break-word
    (1347/1564px overflow) and green on anywhere
    .
  • Full @frontify/fondue-rte Cypress suite: 468/468 (chromium); serializer specs updated to the
    new class. No visual regression — normal text still wraps at word boundaries, only over-long
    tokens break.
  • build, typecheck, lint clean; dist/style.css emits overflow-wrap: anywhere.
    @frontify/fondue-components is unchanged.

Tested locally and it looks like so:

Screen.Recording.2026-07-07.at.13.19.55.mov

Notes for reviewers

  • No shared-primitive changeScrollArea is untouched; the fix is contained to
    RichTextEditor (overflow-wrap: anywhere on paragraphs, list items, and the serializer).
  • In-repo behavioral coverage — both the editor and the rendition paths now have an automated
    wrap test here (LongContentWrapping.cy.tsx), plus a resizable LongContentWrapping Storybook
    story for manual checking.
  • Final confirmation — the fix is proven in Fondue's isolated environment; the web-app comment
    sidebar (its real context) is best confirmed by a live render / e2e there before rollout.

…scrolling horizontally

Long unbreakable strings (URLs, long words) in rich text did not line-break; they
overflowed horizontally and forced a sideways scroll — visible in the comment sidebar
both while typing and after posting.

Two causes:
- The live-editor paragraph and list-item markup elements carried no break rule, so a
  long word could not wrap. List items also render as `inline-flex`, which shrink-wraps
  to max-content and defeats `overflow-wrap` even where the rule is present (the same
  reason the serialized rendition of a list item overflowed) — capping them at the
  container width lets the text wrap.
- ScrollArea's Radix viewport wraps children in an inline `display: table; min-width: 100%`
  element that shrink-wraps to the widest unbreakable token, so a break-enabled paragraph
  never wrapped. Forcing that wrapper to block bounds it to the viewport width. Genuinely
  wide, unshrinkable content (fixed-width media, `nowrap`) still scrolls horizontally.

Added a ScrollArea component test (real browser) asserting long unbreakable text wraps
rather than overflowing; the existing horizontal-scroll tests guard no regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 26c0374

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@frontify/fondue-rte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify

netlify Bot commented Jul 5, 2026

Copy link
Copy Markdown

Deploy Preview for fondue-components ready!

Name Link
🔨 Latest commit 26c0374
🔍 Latest deploy log https://app.netlify.com/projects/fondue-components/deploys/6a4ce12f0c40f500081d9b54
😎 Deploy Preview https://deploy-preview-2781.components.fondue-components.frontify.com
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

@sgruetter sgruetter left a comment

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.

Are the comments helpful? They are surprising (carry information) to some degree, but is it worth it?

… list items

getLicElementClassNames now emits tw-max-w-full, which the serializer also uses;
the three serializeNodeToHtmlRecursive exact-HTML fixtures asserted the pre-change
markup and failed. Match them to the intended serialized output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sgruetter sgruetter marked this pull request as ready for review July 6, 2026 09:05
@sgruetter sgruetter requested review from a team as code owners July 6, 2026 09:05
@sgruetter sgruetter requested review from silviojaeger and syeo66 July 6, 2026 09:05
Comment on lines +19 to +27
/* Radix wraps the viewport children in an inline `display: table; min-width: 100%` element that
shrink-wraps to max-content. That widens the region to fit the longest unbreakable token, so
`overflow-wrap`/`word-break` on descendants never engages and long words/URLs scroll sideways
instead of wrapping. Forcing the wrapper to block caps it at the viewport width so text wraps;
genuinely-wide content (fixed-width media, nowrap) still overflows and scrolls. `!important`
is required to beat Radix's inline style. */
> div {
display: block !important;
}

@jcosta33 jcosta33 Jul 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we usually fix this at the web-app level when an issue manifests itself to avoid overriding radix globally (see NavigationLayout.tsx:l42 for example). It's such a commonplace issue that this might be justified but we have to test very thoroughly to see if it doesn't create other side effects. @noahwaldner wdyt?

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.

I initially wanted to fix it directly in the comment area - and did so here https://github.com/Frontify/web-app/pull/15371 - but then closed it in favor of this change. If the Radix defaults lead to the problem, I'd fix this here (or make a component that wraps this - but then we'd have the same situation), as it otherwise just occurs again and again.

But of course you guys have to say, I'm merely testing the harness (and trying not to waste your time).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At the time we preferred the case by case basis because we were trying to avoid unintended consequences of doing it here, but after all this time we've seen nothing but issues so maybe it's not such a bad idea anymore. I would still loop in @noahwaldner as I haven't worked on fondue in a long time, maybe he has a different take

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This isn't a ScrollArea bug. Radix's display: table wrapper is working as intended for the vast majority of consumers.

The horizontal-scroll issue only surfaces because the legacy RTE emits content that shrink-wraps to a long token. Forcing display: block !important on the viewport wrapper changes behavior for every ScrollArea in the design system to solve a problem that only one type of content has. That's a lot of blast radius (and a Radix-internals coupling we'd have to babysit on upgrades) to fix a defect that lives in RTE.

@silviojaeger silviojaeger requested review from fulopdaniel and removed request for silviojaeger July 6, 2026 12:06
fulopdaniel
fulopdaniel previously approved these changes Jul 6, 2026

@fulopdaniel fulopdaniel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stuff in rte package looks good, but I would remove the comments.

…ly the Radix-override rationale

Apply comment scrutiny (default-to-removed, per-clause sole-means): the rte
comments on tw-max-w-full / tw-break-words restated the code or general CSS, and
the ScrollArea test comment restated its assertion — all removed. The ScrollArea
scss comment collapses to one line: why an anonymous `> div` is force-blocked
with !important (Radix's inline `display: table` wrapper) is the one fact the
code cannot otherwise state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sgruetter

Copy link
Copy Markdown
Contributor Author

@fulopdaniel I treated this change with the latest "no comments by default - only allow then when they are the sole means to understand something" behavior, and it improved it a bit.

@sgruetter sgruetter requested a review from noahwaldner July 7, 2026 07:45

export const ListItemContentMarkupElementNode = ({ attributes, children, element }: PlateRenderElementProps) => {
return (
<p className={getLicElementClassNames(element)} {...attributes}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

break-word doesn't affect the reported min-content width of the text element, it only wraps the text visually, therefore causing the width to still grow.

overflow-wrap: anywhere also lowers the reported min width, but might have other implications that need to be checked

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.

I thought so too, but I couldn't test it manually. How can we test this manually? Maybe it's right somehow as the tests seem to be correct – but I don't believe them in this case.

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.

I could now test it manually and fixed it. You were right (unsurprisingly). The overflow-wrap: anywhere seems to fix it. See the updated description for the fix.

export const ListItemContentMarkupElementNode = ({ attributes, children, element }: PlateRenderElementProps) => {
return (
<p className={getLicElementClassNames(element)} {...attributes}>
<p className={merge([getLicElementClassNames(element), 'tw-break-words'])} {...attributes}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add a behavioral test for this change

…d behavioral test

break-word wraps visually but does not lower min-content width, so long tokens still overflowed inside a shrink-wrapping ScrollArea viewport. Editor paragraphs/list items and the serialized rendition now use overflow-wrap:anywhere, which wraps even in that context; the now-redundant tw-max-w-full is dropped.

Reverts the ScrollArea display:block change — the defect lives in RTE, and forcing block on the shared Radix viewport had a large blast radius.

Adds LongContentWrapping.cy.tsx (editor + rendition, red on break-word, green on anywhere) and a resizable Storybook story.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
};

describe('RichTextEditor wraps long unbreakable content in a shrink-wrapping container', () => {
it('wraps a long token while editing a paragraph', () => {

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.

Do we also have the should house rule in Fondue? Then I'll update this and add it to the harness.

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.

4 participants