fix(RichTextEditor, ScrollArea): wrap long words and URLs instead of scrolling horizontally#2781
fix(RichTextEditor, ScrollArea): wrap long words and URLs instead of scrolling horizontally#2781sgruetter wants to merge 4 commits into
Conversation
…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 detectedLatest commit: 26c0374 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
✅ Deploy Preview for fondue-components ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
sgruetter
left a comment
There was a problem hiding this comment.
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>
| /* 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; | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
fulopdaniel
left a comment
There was a problem hiding this comment.
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>
|
@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. |
|
|
||
| export const ListItemContentMarkupElementNode = ({ attributes, children, element }: PlateRenderElementProps) => { | ||
| return ( | ||
| <p className={getLicElementClassNames(element)} {...attributes}> |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}> |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
Do we also have the should house rule in Fondue? Then I'll update this and add it to the harness.
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, butbreak-wordonly 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 viewportwraps its children in an inline
display: table; min-width: 100%element that shrink-wraps tomax-content. List items compound it by rendering as
inline-flex, which also shrink-wraps.overflow-wrap: anywherewraps likebreak-wordvisually and lowers the reported min-contentwidth, 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: anywhereinstead ofbreak-word.ScrollAreadisplay: block !importantoverride is reverted. The defect lives inRTE, and forcing block on the shared Radix viewport (~66 web-app consumers) was more blast radius
than the fix needs —
overflow-wrap: anywherefixes the content regardless of its container.tw-max-w-fullcap on list items is dropped — redundant once min-content is lowered.Genuinely wide, unshrinkable content (fixed-width media,
nowrap) still scrolls horizontally.Evidence
RichTextEditorcomponent test (real browser,LongContentWrapping.cy.tsx) drives a longunbreakable 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 onbreak-word(1347/1564px overflow) and green on
anywhere.@frontify/fondue-rteCypress suite: 468/468 (chromium); serializer specs updated to thenew class. No visual regression — normal text still wraps at word boundaries, only over-long
tokens break.
build,typecheck,lintclean;dist/style.cssemitsoverflow-wrap: anywhere.@frontify/fondue-componentsis unchanged.Tested locally and it looks like so:
Screen.Recording.2026-07-07.at.13.19.55.mov
Notes for reviewers
ScrollAreais untouched; the fix is contained toRichTextEditor(overflow-wrap: anywhereon paragraphs, list items, and the serializer).wrap test here (
LongContentWrapping.cy.tsx), plus a resizableLongContentWrappingStorybookstory for manual checking.
sidebar (its real context) is best confirmed by a live render / e2e there before rollout.