From 1b0a508fe96be7d86796506a0bb0e1560bc1b403 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 7 Nov 2025 13:37:41 +0000 Subject: [PATCH 01/14] Implementation --- .../SegmentedControl.module.css | 103 +++++++++++++++++- .../src/SegmentedControl/SegmentedControl.tsx | 68 +++++------- 2 files changed, 128 insertions(+), 43 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 2717c72816b..1a26f69a285 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -10,11 +10,85 @@ border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent); border-radius: var(--borderRadius-medium); - &:where([data-full-width]) { + /* Responsive full-width */ + &[data-full-width] { display: flex; width: 100%; } + &[data-full-width-narrow] { + @media (--viewportRange-narrow) { + display: flex; + width: 100%; + } + } + + &[data-full-width-regular] { + @media (--viewportRange-regular) { + display: flex; + width: 100%; + } + } + + &[data-full-width-wide] { + @media (--viewportRange-wide) { + display: flex; + width: 100%; + } + } + + /* Hide when dropdown variant is active */ + &[data-variant-dropdown] { + display: none; + } + + &[data-variant-narrow='dropdown'] { + @media (--viewportRange-narrow) { + display: none; + } + } + + &[data-variant-regular='dropdown'] { + @media (--viewportRange-regular) { + display: none; + } + } + + &[data-variant-wide='dropdown'] { + @media (--viewportRange-wide) { + display: none; + } + } + + /* Handle hideLabels variant - hide button text */ + &[data-variant-hideLabels] .Text { + display: none; + } + + &[data-variant-narrow='hideLabels'] { + @media (--viewportRange-narrow) { + .Text { + display: none; + } + } + } + + &[data-variant-regular='hideLabels'] { + @media (--viewportRange-regular) { + .Text { + display: none; + } + } + } + + &[data-variant-wide='hideLabels'] { + @media (--viewportRange-wide) { + .Text { + display: none; + } + } + } + &:where([data-size='small']) { /* TODO: use primitive `control.{small|medium}.size` when it is available */ height: 28px; @@ -22,6 +96,33 @@ } } +.DropdownContainer { + display: none; + + /* Show when dropdown variant is active */ + &[data-variant='dropdown'] { + display: block; + } + + &[data-variant-narrow='dropdown'] { + @media (--viewportRange-narrow) { + display: block; + } + } + + &[data-variant-regular='dropdown'] { + @media (--viewportRange-regular) { + display: block; + } + } + + &[data-variant-wide='dropdown'] { + @media (--viewportRange-wide) { + display: block; + } + } +} + .Item { position: relative; display: block; diff --git a/packages/react/src/SegmentedControl/SegmentedControl.tsx b/packages/react/src/SegmentedControl/SegmentedControl.tsx index 76d25fbb64c..751629224f4 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.tsx @@ -6,7 +6,7 @@ import SegmentedControlIconButton from './SegmentedControlIconButton' import {ActionList} from '../ActionList' import {ActionMenu} from '../ActionMenu' import type {ResponsiveValue} from '../hooks/useResponsiveValue' -import {useResponsiveValue} from '../hooks/useResponsiveValue' +import {getResponsiveAttributes} from '../internal/utils/getResponsiveAttributes' import type {WidthOnlyViewportRangeKeys} from '../utils/types/ViewportRangeKeys' import {isElement} from 'react-is' import classes from './SegmentedControl.module.css' @@ -45,8 +45,7 @@ const Root: React.FC> = ({ React.Children.toArray(children).some( child => React.isValidElement(child) && child.props.defaultSelected !== undefined, ) - const responsiveVariant = useResponsiveValue(variant, 'default') - const isFullWidth = useResponsiveValue(fullWidth, false) + const selectedSegments = React.Children.toArray(children).map( child => React.isValidElement(child) && @@ -110,9 +109,13 @@ const Root: React.FC> = ({ ) } - return responsiveVariant === 'dropdown' ? ( - // Render the 'dropdown' variant of the SegmentedControlButton or SegmentedControlIconButton - <> + // Check if dropdown variant is used at any breakpoint + const hasDropdownVariant = + typeof variant === 'object' && variant !== null && Object.values(variant).includes('dropdown') + + // Render dropdown variant if needed + const dropdownContent = hasDropdownVariant && ( +
{/* The aria-label is only provided as a backup when the designer or engineer neglects to show a label for the SegmentedControl. @@ -150,15 +153,18 @@ const Root: React.FC> = ({ - - ) : ( - // Render a segmented control +
+ ) + + // Render segmented control (default or hideLabels variant) + const segmentedControlContent = (
    @@ -186,43 +192,21 @@ const Root: React.FC> = ({ }, } - // Render the 'hideLabels' variant of the SegmentedControlButton - if ( - responsiveVariant === 'hideLabels' && - React.isValidElement(child) && - (child.type === Button || isSlot(child, Button)) - ) { - const { - 'aria-label': childAriaLabel, - leadingVisual, - leadingIcon, - children: childPropsChildren, - ...restChildProps - } = child.props - // Use leadingVisual if provided, otherwise fall back to leadingIcon - const visual = leadingVisual ?? leadingIcon - if (!visual) { - // eslint-disable-next-line no-console - console.warn('A `leadingVisual` or `leadingIcon` prop is required when hiding visible labels') - } else { - return ( - - ) - } - } - // Render the children as-is and add the shared child props return React.cloneElement(child, sharedChildProps) })}
) + + // Return both variants when dropdown is used, otherwise just the segmented control + return hasDropdownVariant ? ( + <> + {dropdownContent} + {segmentedControlContent} + + ) : ( + segmentedControlContent + ) } Root.displayName = 'SegmentedControl' From 0f1a1effb8e333dd593298dfd0037f7daaa4b357 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 7 Nov 2025 13:37:43 +0000 Subject: [PATCH 02/14] Add stories --- .../SegmentedControl.responsive.stories.tsx | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx diff --git a/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx new file mode 100644 index 00000000000..c4b20e1cb6d --- /dev/null +++ b/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx @@ -0,0 +1,109 @@ +import type {Meta, StoryFn} from '@storybook/react-vite' +import React from 'react' +import {SegmentedControl} from './SegmentedControl' +import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react' + +const meta: Meta = { + title: 'Components/SegmentedControl/Responsive Tests', + parameters: { + layout: 'padded', + controls: {expanded: true}, + }, +} + +export default meta + +/** + * Test responsive fullWidth behavior. + * Resize the viewport to see the control change width at different breakpoints. + */ +export const FullWidthResponsive: StoryFn = () => ( + + + Preview + + Raw + Blame + +) + +FullWidthResponsive.parameters = { + docs: { + description: { + story: + 'The control fills the full width on **narrow** viewports and uses inline width on **regular** and **wide** viewports.', + }, + }, +} + +/** + * Test responsive variant behavior with hideLabels. + * Resize the viewport to see labels hide/show at different breakpoints. + */ +export const VariantHideLabelsResponsive: StoryFn = () => ( + + + Preview + + Raw + Blame + +) + +VariantHideLabelsResponsive.parameters = { + docs: { + description: { + story: + 'Labels are **hidden** (icon-only) on narrow viewports and **visible** on regular and wide viewports. Note: leadingVisual prop is required for hideLabels variant.', + }, + }, +} + +/** + * Test responsive variant behavior with dropdown. + * Resize the viewport to see the control switch between dropdown and buttons. + */ +export const VariantDropdownResponsive: StoryFn = () => ( + + + Preview + + Raw + Blame + +) + +VariantDropdownResponsive.parameters = { + docs: { + description: { + story: + 'Renders as a **dropdown menu** on narrow viewports and as **segmented buttons** on regular and wide viewports.', + }, + }, +} + +/** + * Test complex responsive behavior combining fullWidth and variant. + */ +export const ComplexResponsive: StoryFn = () => ( + + + Preview + + Raw + Blame + +) + +ComplexResponsive.parameters = { + docs: { + description: { + story: + 'Complex: **full-width + icon-only** (narrow) → **full-width + labels** (regular) → **inline + labels** (wide)', + }, + }, +} From 4c22f758fb99fc6fc591f251b5de4e440dc69b7c Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 7 Nov 2025 13:38:42 +0000 Subject: [PATCH 03/14] Add changelog --- .changeset/segmentedcontrol-responsive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/segmentedcontrol-responsive.md diff --git a/.changeset/segmentedcontrol-responsive.md b/.changeset/segmentedcontrol-responsive.md new file mode 100644 index 00000000000..36fa3a6caf3 --- /dev/null +++ b/.changeset/segmentedcontrol-responsive.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +SegmentedControl: Remove useResponsiveValue hook from fullWidth and variant props to use `getResponsiveAttributes` instead. From cd04070e22417e4d9e499e6b96d16ba1c15a2889 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 7 Nov 2025 14:15:42 +0000 Subject: [PATCH 04/14] Improve stories --- .../SegmentedControl.responsive.stories.tsx | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx index c4b20e1cb6d..25143e39f40 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx @@ -18,13 +18,16 @@ export default meta * Resize the viewport to see the control change width at different breakpoints. */ export const FullWidthResponsive: StoryFn = () => ( - - - Preview - - Raw - Blame - +
+

Full width: yes (narrow) → no (regular + wide)

+ + + Preview + + Raw + Blame + +
) FullWidthResponsive.parameters = { @@ -41,13 +44,16 @@ FullWidthResponsive.parameters = { * Resize the viewport to see labels hide/show at different breakpoints. */ export const VariantHideLabelsResponsive: StoryFn = () => ( - - - Preview - - Raw - Blame - +
+

Labels: hidden (narrow) → visible (regular + wide)

+ + + Preview + + Raw + Blame + +
) VariantHideLabelsResponsive.parameters = { @@ -64,13 +70,16 @@ VariantHideLabelsResponsive.parameters = { * Resize the viewport to see the control switch between dropdown and buttons. */ export const VariantDropdownResponsive: StoryFn = () => ( - - - Preview - - Raw - Blame - +
+

Variant: dropdown (narrow) → buttons (regular + wide)

+ + + Preview + + Raw + Blame + +
) VariantDropdownResponsive.parameters = { @@ -86,17 +95,22 @@ VariantDropdownResponsive.parameters = { * Test complex responsive behavior combining fullWidth and variant. */ export const ComplexResponsive: StoryFn = () => ( - - - Preview - - Raw - Blame - +
+

+ Complex: full-width + icon-only (narrow) → full-width + labels (regular) → inline + labels (wide) +

+ + + Preview + + Raw + Blame + +
) ComplexResponsive.parameters = { From 19c9113809cf95af53ee9ab7d9c3476e512954db Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 7 Nov 2025 14:29:08 +0000 Subject: [PATCH 05/14] Fix fullWidth rules --- .../SegmentedControl.module.css | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 1a26f69a285..18fa123555f 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -11,34 +11,42 @@ border-radius: var(--borderRadius-medium); /* Responsive full-width */ - &[data-full-width] { + &[data-full-width='true'] { display: flex; width: 100%; } - &[data-full-width-narrow] { + &[data-full-width-narrow='true'] { @media (--viewportRange-narrow) { display: flex; width: 100%; } } - &[data-full-width-regular] { + &[data-full-width-regular='true'] { @media (--viewportRange-regular) { display: flex; width: 100%; } } - &[data-full-width-wide] { + &[data-full-width-wide='true'] { @media (--viewportRange-wide) { display: flex; width: 100%; } } + /* Reset when full-width disabled on wider breakpoints */ + &[data-full-width-regular='true'][data-full-width-wide='false'] { + @media (--viewportRange-wide) { + display: inline-flex; + width: auto; + } + } + /* Hide when dropdown variant is active */ - &[data-variant-dropdown] { + &[data-variant='dropdown'] { display: none; } @@ -61,7 +69,7 @@ } /* Handle hideLabels variant - hide button text */ - &[data-variant-hideLabels] .Text { + &[data-variant='hideLabels'] .Text { display: none; } @@ -242,9 +250,31 @@ /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ width: 32px; - .SegmentedControl:where([data-full-width]) & { + .SegmentedControl[data-full-width='true'] & { width: 100%; } + + @media (--viewportRange-narrow) { + .SegmentedControl[data-full-width-narrow='true'] & { + width: 100%; + } + } + + @media (--viewportRange-regular) { + .SegmentedControl[data-full-width-regular='true'] & { + width: 100%; + } + } + + @media (--viewportRange-wide) { + .SegmentedControl[data-full-width-wide='true'] & { + width: 100%; + } + + .SegmentedControl[data-full-width-regular='true'][data-full-width-wide='false'] & { + width: 32px; + } + } } .Content { From 42c5bde36155de2e4c9f8320742347a46b2d1188 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 7 Nov 2025 14:29:54 +0000 Subject: [PATCH 06/14] Reorganize css --- .../SegmentedControl.module.css | 136 ++++++++---------- 1 file changed, 63 insertions(+), 73 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index 18fa123555f..c63531e56e1 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -1,4 +1,7 @@ .SegmentedControl { + /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ + --segmented-control-icon-width: 32px; + display: inline-flex; /* TODO: use primitive `control.{small|medium}.size` when it is available */ @@ -14,34 +17,60 @@ &[data-full-width='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; + } + + &[data-full-width='false'] { + display: inline-flex; + width: auto; + --segmented-control-icon-width: 32px; } - &[data-full-width-narrow='true'] { - @media (--viewportRange-narrow) { + @media (--viewportRange-narrow) { + &[data-full-width-narrow='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; + } + + &[data-full-width-narrow='false'] { + display: inline-flex; + width: auto; + --segmented-control-icon-width: 32px; } } - &[data-full-width-regular='true'] { - @media (--viewportRange-regular) { + @media (--viewportRange-regular) { + &[data-full-width-regular='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; + } + + &[data-full-width-regular='false'] { + display: inline-flex; + width: auto; + --segmented-control-icon-width: 32px; } } - &[data-full-width-wide='true'] { - @media (--viewportRange-wide) { + @media (--viewportRange-wide) { + &[data-full-width-wide='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; } - } - /* Reset when full-width disabled on wider breakpoints */ - &[data-full-width-regular='true'][data-full-width-wide='false'] { - @media (--viewportRange-wide) { + &[data-full-width-wide='false'] { display: inline-flex; width: auto; + --segmented-control-icon-width: 32px; + } + + &[data-full-width-regular='true']:not([data-full-width-wide='true']) { + display: inline-flex; + width: auto; + --segmented-control-icon-width: 32px; } } @@ -50,50 +79,38 @@ display: none; } - &[data-variant-narrow='dropdown'] { - @media (--viewportRange-narrow) { - display: none; - } + /* Handle hideLabels variant - hide button text */ + &[data-variant='hideLabels'] .Text { + display: none; } - &[data-variant-regular='dropdown'] { - @media (--viewportRange-regular) { + @media (--viewportRange-narrow) { + &[data-variant-narrow='dropdown'] { display: none; } - } - &[data-variant-wide='dropdown'] { - @media (--viewportRange-wide) { + &[data-variant-narrow='hideLabels'] .Text { display: none; } } - /* Handle hideLabels variant - hide button text */ - &[data-variant='hideLabels'] .Text { - display: none; - } + @media (--viewportRange-regular) { + &[data-variant-regular='dropdown'] { + display: none; + } - &[data-variant-narrow='hideLabels'] { - @media (--viewportRange-narrow) { - .Text { - display: none; - } + &[data-variant-regular='hideLabels'] .Text { + display: none; } } - &[data-variant-regular='hideLabels'] { - @media (--viewportRange-regular) { - .Text { - display: none; - } + @media (--viewportRange-wide) { + &[data-variant-wide='dropdown'] { + display: none; } - } - &[data-variant-wide='hideLabels'] { - @media (--viewportRange-wide) { - .Text { - display: none; - } + &[data-variant-wide='hideLabels'] .Text { + display: none; } } @@ -112,20 +129,20 @@ display: block; } - &[data-variant-narrow='dropdown'] { - @media (--viewportRange-narrow) { + @media (--viewportRange-narrow) { + &[data-variant-narrow='dropdown'] { display: block; } } - &[data-variant-regular='dropdown'] { - @media (--viewportRange-regular) { + @media (--viewportRange-regular) { + &[data-variant-regular='dropdown'] { display: block; } } - &[data-variant-wide='dropdown'] { - @media (--viewportRange-wide) { + @media (--viewportRange-wide) { + &[data-variant-wide='dropdown'] { display: block; } } @@ -247,34 +264,7 @@ } .IconButton { - /* TODO: use primitive `control.medium.size` when it is available instead of '32px' */ - width: 32px; - - .SegmentedControl[data-full-width='true'] & { - width: 100%; - } - - @media (--viewportRange-narrow) { - .SegmentedControl[data-full-width-narrow='true'] & { - width: 100%; - } - } - - @media (--viewportRange-regular) { - .SegmentedControl[data-full-width-regular='true'] & { - width: 100%; - } - } - - @media (--viewportRange-wide) { - .SegmentedControl[data-full-width-wide='true'] & { - width: 100%; - } - - .SegmentedControl[data-full-width-regular='true'][data-full-width-wide='false'] & { - width: 32px; - } - } + width: var(--segmented-control-icon-width, 32px); } .Content { From 993431ca6ae276cc7bc1c0252ce42ad388bd4bcd Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Fri, 7 Nov 2025 15:37:21 +0100 Subject: [PATCH 07/14] Update packages/react/src/SegmentedControl/SegmentedControl.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/react/src/SegmentedControl/SegmentedControl.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.tsx b/packages/react/src/SegmentedControl/SegmentedControl.tsx index 751629224f4..fb5025d15d8 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.tsx @@ -111,7 +111,8 @@ const Root: React.FC> = ({ // Check if dropdown variant is used at any breakpoint const hasDropdownVariant = - typeof variant === 'object' && variant !== null && Object.values(variant).includes('dropdown') + variant === 'dropdown' || + (typeof variant === 'object' && variant !== null && Object.values(variant).includes('dropdown')) // Render dropdown variant if needed const dropdownContent = hasDropdownVariant && ( From baa385a020103e9c74d58b9b075610928a7da107 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 11 Nov 2025 15:05:22 +0000 Subject: [PATCH 08/14] Fix types --- packages/react/src/SegmentedControl/SegmentedControl.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.tsx b/packages/react/src/SegmentedControl/SegmentedControl.tsx index fb5025d15d8..751629224f4 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.tsx @@ -111,8 +111,7 @@ const Root: React.FC> = ({ // Check if dropdown variant is used at any breakpoint const hasDropdownVariant = - variant === 'dropdown' || - (typeof variant === 'object' && variant !== null && Object.values(variant).includes('dropdown')) + typeof variant === 'object' && variant !== null && Object.values(variant).includes('dropdown') // Render dropdown variant if needed const dropdownContent = hasDropdownVariant && ( From 18a5499d241550fde67a4a0112a125f0f5101010 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 11 Nov 2025 15:56:03 +0000 Subject: [PATCH 09/14] Fix types --- packages/react/src/SegmentedControl/SegmentedControl.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.tsx b/packages/react/src/SegmentedControl/SegmentedControl.tsx index 751629224f4..fe2a9b5a81b 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.tsx @@ -110,8 +110,8 @@ const Root: React.FC> = ({ } // Check if dropdown variant is used at any breakpoint - const hasDropdownVariant = - typeof variant === 'object' && variant !== null && Object.values(variant).includes('dropdown') + const responsiveVariant = typeof variant === 'object' ? variant : undefined + const hasDropdownVariant = responsiveVariant ? Object.values(responsiveVariant).includes('dropdown') : false // Render dropdown variant if needed const dropdownContent = hasDropdownVariant && ( From 4b58f33a37868509a3e6bfe3aa2f8071f2076018 Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 11 Nov 2025 16:01:37 +0000 Subject: [PATCH 10/14] Fix stylelint --- .../src/SegmentedControl/SegmentedControl.module.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.module.css b/packages/react/src/SegmentedControl/SegmentedControl.module.css index c63531e56e1..77270b6d227 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.module.css +++ b/packages/react/src/SegmentedControl/SegmentedControl.module.css @@ -17,12 +17,14 @@ &[data-full-width='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; } &[data-full-width='false'] { display: inline-flex; width: auto; + --segmented-control-icon-width: 32px; } @@ -30,12 +32,14 @@ &[data-full-width-narrow='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; } &[data-full-width-narrow='false'] { display: inline-flex; width: auto; + --segmented-control-icon-width: 32px; } } @@ -44,12 +48,14 @@ &[data-full-width-regular='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; } &[data-full-width-regular='false'] { display: inline-flex; width: auto; + --segmented-control-icon-width: 32px; } } @@ -58,18 +64,21 @@ &[data-full-width-wide='true'] { display: flex; width: 100%; + --segmented-control-icon-width: 100%; } &[data-full-width-wide='false'] { display: inline-flex; width: auto; + --segmented-control-icon-width: 32px; } &[data-full-width-regular='true']:not([data-full-width-wide='true']) { display: inline-flex; width: auto; + --segmented-control-icon-width: 32px; } } From 5a996e8bf1da66174ba9e5796dbd179d673d359e Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 11 Nov 2025 16:45:59 +0000 Subject: [PATCH 11/14] Add story for {narrow: 'dropdown'} --- .../SegmentedControl.responsive.stories.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx b/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx index 25143e39f40..5b1099d59b4 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.responsive.stories.tsx @@ -91,6 +91,31 @@ VariantDropdownResponsive.parameters = { }, } +/** + * Test responsive variant behavior when only the narrow breakpoint is specified. + */ +export const VariantDropdownNarrowOnly: StoryFn = () => ( +
+

Variant: dropdown (narrow) → buttons (others inherit default)

+ + + Preview + + Raw + Blame + +
+) + +VariantDropdownNarrowOnly.parameters = { + docs: { + description: { + story: + 'Only the **narrow** breakpoint sets the dropdown variant; wider breakpoints fall back to the default segmented buttons.', + }, + }, +} + /** * Test complex responsive behavior combining fullWidth and variant. */ From ed0ac849119a4c938728c024c3bc263d297458eb Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 11 Nov 2025 16:53:43 +0000 Subject: [PATCH 12/14] Fix tests --- .../react/src/SegmentedControl/SegmentedControl.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx index 0c1fd59a9b1..d97d482e90f 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx @@ -64,7 +64,7 @@ describe('SegmentedControl', () => { }) it('renders the dropdown variant', () => { - const {getByText} = render( + const {getByRole} = render( {segmentData.map(({label}, index) => ( @@ -73,7 +73,7 @@ describe('SegmentedControl', () => { ))} , ) - const button = getByText(segmentData[1].label) + const button = getByRole('button', {name: `${segmentData[1].label}, File view`}) expect(button).toBeInTheDocument() expect(button.closest('button')?.getAttribute('aria-haspopup')).toBe('true') @@ -280,7 +280,7 @@ describe('SegmentedControl', () => { , ) - const button = component.getByText(segmentData[0].label) + const button = component.getByRole('button', {name: `${segmentData[0].label}, File view`}) fireEvent.click(button) expect(handleChange).not.toHaveBeenCalled() @@ -303,7 +303,7 @@ describe('SegmentedControl', () => { , ) - const button = component.getByText(segmentData[0].label) + const button = component.getByRole('button', {name: `${segmentData[0].label}, File view`}) fireEvent.click(button) expect(handleClick).not.toHaveBeenCalled() From d27c6d003f134c2d566be3464132735e650accfc Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 11 Nov 2025 17:06:27 +0000 Subject: [PATCH 13/14] Remove outdated test --- .../SegmentedControl/SegmentedControl.test.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx index d97d482e90f..0778ef8c17a 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx @@ -313,23 +313,6 @@ describe('SegmentedControl', () => { expect(handleClick).toHaveBeenCalled() }) - it('warns users if they try to use the hideLabels variant without a leadingVisual', () => { - const spy = vi.spyOn(globalThis.console, 'warn').mockImplementation(() => {}) - - render( - - {segmentData.map(({label}, index) => ( - - {label} - - ))} - , - ) - - expect(spy).toHaveBeenCalledTimes(3) - spy.mockRestore() - }) - it('supports deprecated leadingIcon prop for backward compatibility', () => { const {getByText} = render( From acd98303e780358ab0eefe3f1fbb10d8e3ebfe2c Mon Sep 17 00:00:00 2001 From: Hector Garcia Date: Tue, 11 Nov 2025 17:08:54 +0000 Subject: [PATCH 14/14] Adapt test for proper aria labels on icon buttons --- .../react/src/SegmentedControl/SegmentedControl.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx index 0778ef8c17a..284cc9a1906 100644 --- a/packages/react/src/SegmentedControl/SegmentedControl.test.tsx +++ b/packages/react/src/SegmentedControl/SegmentedControl.test.tsx @@ -80,7 +80,7 @@ describe('SegmentedControl', () => { }) it('renders the hideLabels variant', () => { - const {getByLabelText} = render( + const {getByRole} = render( {segmentData.map(({label, icon}, index) => ( @@ -91,8 +91,8 @@ describe('SegmentedControl', () => { ) for (const datum of segmentData) { - const labelledButton = getByLabelText(datum.label) - expect(labelledButton).toBeDefined() + const labelledButton = getByRole('button', {name: datum.iconLabel}) + expect(labelledButton).toBeInTheDocument() } })