Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/segmentedcontrol-responsive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

SegmentedControl: Remove useResponsiveValue hook from fullWidth and variant props to use `getResponsiveAttributes` instead.
144 changes: 137 additions & 7 deletions packages/react/src/SegmentedControl/SegmentedControl.module.css
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -10,9 +13,114 @@
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='true'] {
display: flex;
width: 100%;

--segmented-control-icon-width: 100%;
}

&[data-full-width='false'] {
display: inline-flex;
width: auto;

--segmented-control-icon-width: 32px;
}

@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;
}
}

@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;
}
}

@media (--viewportRange-wide) {
&[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;
}
}

/* Hide when dropdown variant is active */
&[data-variant='dropdown'] {
display: none;
}

/* Handle hideLabels variant - hide button text */
&[data-variant='hideLabels'] .Text {
display: none;
}

@media (--viewportRange-narrow) {
&[data-variant-narrow='dropdown'] {
display: none;
}

&[data-variant-narrow='hideLabels'] .Text {
display: none;
}
}

@media (--viewportRange-regular) {
&[data-variant-regular='dropdown'] {
display: none;
}

&[data-variant-regular='hideLabels'] .Text {
display: none;
}
}

@media (--viewportRange-wide) {
&[data-variant-wide='dropdown'] {
display: none;
}

&[data-variant-wide='hideLabels'] .Text {
display: none;
}
}

&:where([data-size='small']) {
Expand All @@ -22,6 +130,33 @@
}
}

.DropdownContainer {
display: none;

/* Show when dropdown variant is active */
&[data-variant='dropdown'] {
display: block;
}

@media (--viewportRange-narrow) {
&[data-variant-narrow='dropdown'] {
display: block;
}
}

@media (--viewportRange-regular) {
&[data-variant-regular='dropdown'] {
display: block;
}
}

@media (--viewportRange-wide) {
&[data-variant-wide='dropdown'] {
display: block;
}
}
}

.Item {
position: relative;
display: block;
Expand Down Expand Up @@ -138,12 +273,7 @@
}

.IconButton {
/* TODO: use primitive `control.medium.size` when it is available instead of '32px' */
width: 32px;

.SegmentedControl:where([data-full-width]) & {
width: 100%;
}
width: var(--segmented-control-icon-width, 32px);
}

.Content {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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 = () => (
Copy link
Member

Choose a reason for hiding this comment

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

reminder to add to VRT if desired

<div>
<p style={{marginBottom: '16px'}}>Full width: yes (narrow) → no (regular + wide)</p>
<SegmentedControl aria-label="File view" fullWidth={{narrow: true, regular: false, wide: false}}>
<SegmentedControl.Button defaultSelected leadingVisual={EyeIcon}>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={FileCodeIcon}>Raw</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={PeopleIcon}>Blame</SegmentedControl.Button>
</SegmentedControl>
</div>
)

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 = () => (
<div>
<p style={{marginBottom: '16px'}}>Labels: hidden (narrow) → visible (regular + wide)</p>
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels', regular: 'default', wide: 'default'}}>
<SegmentedControl.Button defaultSelected leadingVisual={EyeIcon}>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={FileCodeIcon}>Raw</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={PeopleIcon}>Blame</SegmentedControl.Button>
</SegmentedControl>
</div>
)

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 = () => (
<div>
<p style={{marginBottom: '16px'}}>Variant: dropdown (narrow) → buttons (regular + wide)</p>
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown', regular: 'default', wide: 'default'}}>
<SegmentedControl.Button defaultSelected leadingVisual={EyeIcon}>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={FileCodeIcon}>Raw</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={PeopleIcon}>Blame</SegmentedControl.Button>
</SegmentedControl>
</div>
)

VariantDropdownResponsive.parameters = {
docs: {
description: {
story:
'Renders as a **dropdown menu** on narrow viewports and as **segmented buttons** on regular and wide viewports.',
},
},
}

/**
* Test responsive variant behavior when only the narrow breakpoint is specified.
*/
export const VariantDropdownNarrowOnly: StoryFn = () => (
<div>
<p style={{marginBottom: '16px'}}>Variant: dropdown (narrow) → buttons (others inherit default)</p>
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown'}}>
<SegmentedControl.Button defaultSelected leadingVisual={EyeIcon}>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={FileCodeIcon}>Raw</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={PeopleIcon}>Blame</SegmentedControl.Button>
</SegmentedControl>
</div>
)

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.
*/
export const ComplexResponsive: StoryFn = () => (
<div>
<p style={{marginBottom: '16px'}}>
Complex: full-width + icon-only (narrow) → full-width + labels (regular) → inline + labels (wide)
</p>
<SegmentedControl
aria-label="File view"
fullWidth={{narrow: true, regular: true, wide: false}}
variant={{narrow: 'hideLabels', regular: 'default', wide: 'default'}}
>
<SegmentedControl.Button defaultSelected leadingVisual={EyeIcon}>
Preview
</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={FileCodeIcon}>Raw</SegmentedControl.Button>
<SegmentedControl.Button leadingVisual={PeopleIcon}>Blame</SegmentedControl.Button>
</SegmentedControl>
</div>
)

ComplexResponsive.parameters = {
docs: {
description: {
story:
'Complex: **full-width + icon-only** (narrow) → **full-width + labels** (regular) → **inline + labels** (wide)',
},
},
}
Loading
Loading