feat(segmented-controller): collapsing icon-first variant with reserved width#187
Conversation
…ed width Adds `collapseInactive` to SegmentedController and an explicit `icon` prop on SegmentedControllerItem. When the controller has `collapseInactive` set, only the selected segment shows its label; the rest collapse to icon-only buttons that surface their label via tooltip on hover or keyboard focus. The container measures every segment's expanded and collapsed widths and reserves a worst-case `min-width` so the outer footprint stays stable as selection moves between short and long labels. Labels animate `width: auto ↔ 0` on the same 200ms timeline as the selection indicator (relies on `interpolate-size`, falls back to an abrupt swap on older browsers), and `prefers-reduced-motion` is honored. Resolves DF-4867. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 50c6640 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 |
rmarkins-godaddy
left a comment
There was a problem hiding this comment.
@srice-godaddy Your PR has lockfile changes that I don't think were necessary as part of the work you've done. Please undo that so we can have the test suite run correctly. Also
| {icon !== undefined ? ( | ||
| <> | ||
| {icon} | ||
| {children !== undefined ? ( | ||
| <span className={cx(styles.label, !showLabel && styles.labelHidden)}>{children}</span> | ||
| ) : null} | ||
| </> | ||
| ) : ( | ||
| children | ||
| )} |
There was a problem hiding this comment.
Do not nest ternary statements. If that is needed please move this to a function with if/else logic for better readability
| const out: { value: string; icon: ReactNode; label: ReactNode }[] = []; | ||
| React.Children.toArray(children).forEach(function visit(child) { | ||
| if (!React.isValidElement(child) || child.type !== SegmentedControllerItem) return; | ||
| const p = child.props as SegmentedControllerItemProps; |
There was a problem hiding this comment.
No single letter variables
| const padInline = | ||
| (parseFloat(cs.paddingInlineStart || cs.paddingLeft || '0') || 0) + | ||
| (parseFloat(cs.paddingInlineEnd || cs.paddingRight || '0') || 0); | ||
| const n = measureItems!.length; |
| {measureItems ? ( | ||
| <div ref={mirrorRef} aria-hidden="true" className={styles.mirror}> | ||
| {measureItems.map(function expanded(item) { | ||
| return ( | ||
| <Flex | ||
| as="span" | ||
| key={`e-${item.value}`} | ||
| alignItems="center" | ||
| gap="xs" | ||
| className={styles.item} | ||
| data-measure="expanded" | ||
| data-value={item.value} | ||
| > | ||
| {item.icon} | ||
| <span className={styles.label}>{item.label}</span> | ||
| </Flex> | ||
| ); | ||
| })} | ||
| {measureItems.map(function collapsed(item) { | ||
| return ( | ||
| <Flex | ||
| as="span" | ||
| key={`c-${item.value}`} | ||
| alignItems="center" | ||
| gap="xs" | ||
| className={styles.item} | ||
| data-measure="collapsed" | ||
| data-value={item.value} | ||
| > | ||
| {item.icon} | ||
| </Flex> | ||
| ); | ||
| })} | ||
| </div> | ||
| ) : null} |
There was a problem hiding this comment.
Please move this out of children and into a sub function w/ if/else logic
https://www.loom.com/share/cfaa58c3858840d5ba8ddcaabeed9340
Summary
Implements the icon-first compact variant of
SegmentedControllerfor the Airo agent UI work tracked in DF-4867 (under epic DF-4700). WhencollapseInactiveis set, only the selected segment shows its label; the rest collapse to icon-only buttons that surface their label via tooltip on hover or keyboard focus, and the container reserves a stablemin-widthso the surrounding layout doesn't shift as selection moves between short and long labels.Changes Made
SegmentedControllerItem: new optionaliconprop. Required when the parent hascollapseInactive(dev warning + label-stays-visible fallback if missing). Existing call-sites that put<Icon />directly insidechildrenkeep working unchanged.SegmentedController: newcollapseInactiveboolean prop. Internal context tracks the selected value and drives label visibility and tooltip enabling per item.collapseInactiveis on, a hidden,aria-hiddenmirror renders each item twice (expanded and collapsed) and the controller measures both states viaResizeObserver+getBoundingClientRect. The worst-case width is applied asmin-widthon the inner content. Buttons distribute viajustify-content: space-betweenso the first segment is always at the leading edge and the last at the trailing edge.width: auto ↔ 0(using Baseline-2024interpolate-size: allow-keywords) plusopacity, and the icon/label gap closes to 0 when collapsed — all on the same 200ms timeline as the selection indicator. Falls back to an abrupt swap on browsers withoutinterpolate-size.prefers-reduced-motiondisables the transitions.aria-labelis set on collapsed items so the accessible name survives thewidth: 0collapse.TooltipTriggerisisDisabledfor the selected item so it never shows its own tooltip.Collapse Inactivestory + README section, playground updated with the new flag.min-width, and stable container footprint across selections.Test Plan
nx run @godaddy/antares:test— all green at 100% coveragecomponents/SegmentedController→CollapseInactive):Basic,Controlled,Sizes,Icon,IconOnly,Disabled,Overflow,RTLexamples regression-checked via SSR snapshotsNotes for Reviewers
interpolate-size: allow-keywords(Baseline 2024). Older browsers gracefully degrade to an abrupt label swap — collapse functionality is unchanged, just no smooth animation.package-lock.jsonis included with peer-dep normalization that surfaced after runningnpm installduring this work.🤖 Generated with Claude Code