Skip to content

fix(Navigation): keep the badge in the item's line and truncate the label - #3070

Open
Lisa18289 wants to merge 6 commits into
mainfrom
claude/navigation-badge-truncate-a9d034
Open

fix(Navigation): keep the badge in the item's line and truncate the label#3070
Lisa18289 wants to merge 6 commits into
mainfrom
claude/navigation-badge-truncate-a9d034

Conversation

@Lisa18289

Copy link
Copy Markdown
Member

#2903 let a navigation item wrap so its badge could move to the next line. In a narrow navigation that wraps the label off its icon instead, so keep everything on one line and truncate the label.

A label passed as raw text is an anonymous flex item that no rule can reach — the link's props context wraps it in an element. Remote delivers text as a renderer element rather than a string, so that case is matched too; without it only Local truncated. min-width: 0 on the navigation is what makes truncation happen at all inside a flexible container: a 1fr grid track takes the content's min-content width as its minimum, so the navigation grew past its column instead of shrinking.

The narrow container visual scenario now uses labels that actually overflow — before, every label fit and the scenario could not have shown the difference — with and without a leading icon, plus a Truncate child (what mStudio passes).

Needs the update-screenshots label for the Linux baselines; only the -darwin ones can be written locally.

@Lisa18289 Lisa18289 self-assigned this Sep 2, 2026
@Lisa18289 Lisa18289 added the run-visual-tests Runs the full visual regression suite against the existing baselines and fails the check on mismatch label Sep 2, 2026
@github-actions github-actions Bot removed the run-visual-tests Runs the full visual regression suite against the existing baselines and fails the check on mismatch label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for ./packages/components/

Status Category Percentage Covered / Total
🔵 Lines 78.69% 746 / 948
🔵 Statements 78.57% 763 / 971
🔵 Functions 80.09% 165 / 206
🔵 Branches 70.33% 377 / 536
File CoverageNo changed files found.
Generated in workflow #6669 for commit 08926fb by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

Preview environments are ready:

Type URL
docs pr-3070.docs.review.flow-components.de
storybook pr-3070.storybook.review.flow-components.de

Images:

  • docs: ghcr.io/mittwald/flow/docs:pr-3070
  • storybook: ghcr.io/mittwald/flow/storybook:pr-3070

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

❌ Visual Regression Tests Failed

At least one shard did not pass. If snapshots differ from the committed baselines, download the visual-diffs-* artifacts from this run to inspect the actual/diff images (a shard that failed before comparison, e.g. during install or browser setup, leaves none).

If the differences are intentional, update the baselines by adding the update-screenshots label to the PR.

Run details

@Lisa18289 Lisa18289 added the update-screenshots Label a PR to update the screenshots used for visual regression testing label Sep 2, 2026
@github-actions github-actions Bot removed the update-screenshots Label a PR to update the screenshots used for visual regression testing label Sep 2, 2026
@Lisa18289
Lisa18289 marked this pull request as ready for review September 2, 2026 09:38
@Lisa18289
Lisa18289 requested a review from a team September 2, 2026 09:38
@Lisa18289
Lisa18289 enabled auto-merge (squash) September 2, 2026 09:47
@Lisa18289
Lisa18289 force-pushed the claude/navigation-badge-truncate-a9d034 branch from 349bafa to c686e27 Compare September 3, 2026 11:12
Lisa18289 and others added 2 commits September 3, 2026 13:15
…abel

#2903 let the item wrap so a badge moved to the next line. In a narrow
navigation that broke the label off the icon instead, so keep one line and
truncate the label.

A label passed as raw text is an anonymous flex item that no rule can reach,
so the link's props context wraps it in an element. Remote delivers text as a
renderer element rather than a string — without that branch only Local
truncated.

`min-width: 0` on the navigation is what makes truncation happen at all in a
flexible container: a `1fr` grid track takes the content's min-content width as
its minimum, so the navigation grew past its column instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Lisa18289 <84317589+Lisa18289@users.noreply.github.com>
@Lisa18289
Lisa18289 force-pushed the claude/navigation-badge-truncate-a9d034 branch from c686e27 to 8a00734 Compare September 3, 2026 11:15
Comment on lines +21 to +34
/** Text a link receives as a raw string, or as a remote text node. */
const isText = (child: ReactNode): boolean =>
typeof child === "string" ||
typeof child === "number" ||
(isValidElement(child) && isRemoteTextRenderProps(child.props));

/**
* A bare text node is an anonymous flex item that no rule can reach, so give
* the label an element the item can truncate.
*/
const wrapTextInLabel = (children: ReactNode): ReactNode =>
Children.map(children, (child) =>
isText(child) ? <span>{child}</span> : child,
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

isText duplicates a predicate two other open PRs also carry, and one of them already puts it in the right place.

containsTextChild applied per child answers exactly the question here — containsTextChild("Galactic") is true, containsTextChild(<Badge/>) is false — so both local helpers collapse into one call:

Suggested change
/** Text a link receives as a raw string, or as a remote text node. */
const isText = (child: ReactNode): boolean =>
typeof child === "string" ||
typeof child === "number" ||
(isValidElement(child) && isRemoteTextRenderProps(child.props));
/**
* A bare text node is an anonymous flex item that no rule can reach, so give
* the label an element the item can truncate.
*/
const wrapTextInLabel = (children: ReactNode): ReactNode =>
Children.map(children, (child) =>
isText(child) ? <span>{child}</span> : child,
);
/**
* A bare text node is an anonymous flex item that no rule can reach, so give
* the label an element the item can truncate.
*/
const wrapTextInLabel = (children: ReactNode): ReactNode =>
Children.map(children, (child) =>
containsTextChild(child) ? <span>{child}</span> : child,
);

Two imports follow: line 13 becomes import { containsTextChild } from "@/lib/react/remote";, and isValidElement on line 2 falls away (import { Children } from "react";).

Worth having beyond the deduplication: containsTextChild looks through fragments, so a label passed as <>{"Text"}</> gets wrapped and truncated too. isText returns false for that today.

No rendered output changes for the scenarios in this PR, so the committed baselines stay valid.

Order: this waits on #3053. If that one stalls, merging here as-is is fine — the duplication is then temporary and whichever of #3053/#3076 lands second cleans it up (those two conflict in remote.ts and need a rebase against each other anyway).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Applied in 08926fb, after merging #3053's main in.

I first went down a wrong path here and want to record why, because it nearly cost this PR the change. I read containsTextChild as an aggregate over a whole subtree and concluded it could not answer a per-child question, so I exported isTextNode instead. That was wrong on both counts — and isTextNode is the one symbol #3076 removes, so it would have left this PR depending on something that disappears when yours lands. containsTextChild stays exported there.

Applied per child it is exactly right, verified:

child result
"Galactic" true
{42} true
<Badge /> false
<Badge>New</Badge> false
<>{"Text"}</> true

The fourth line is the one I had doubted — text inside an element stays that element's, so it is not wrapped. And the fifth is the improvement you mentioned; the local helper returned false for it.

One note for when #3076 lands: your containsTextChild is extractTextFromChildren(children) !== undefined, which trims, so a whitespace-only child ({" "} between two elements) stops counting as text and no longer gets a <span>. That reads like the better behaviour to me — flagging it only because it is a silent change to this call site.

test:compile and lint clean, unit 253/253, the Navigation visual scenarios pass against the committed baselines unchanged.

Lisa18289 and others added 3 commits September 4, 2026 08:41
#3053 landed the same predicate in `remote.ts` for the button's text marker.
Export it and drop the local copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mfal
mfal previously approved these changes Sep 4, 2026
#3053 landed the same predicate in `remote.ts`. Applied per child it answers
exactly this question, and it looks through fragments, so `<>{"Text"}</>` is
wrapped and truncated too — the local helper returned false for that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.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.

2 participants