Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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.
135 changes: 128 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,105 @@
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 +121,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 +264,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,123 @@
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 = () => (
<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 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