Skip to content

Commit 7edf134

Browse files
mperrotticolebemis
andauthored
SegmentedControl - make fullWidth prop responsive (#2191)
* updates fullWidth prop to be responsive, replaces useMatchMedia with new useResponsive hook * adds changeset * Update src/SegmentedControl/SegmentedControl.tsx Co-authored-by: Cole Bemis <colebemis@github.com> * Update docs/content/SegmentedControl.mdx Co-authored-by: Cole Bemis <colebemis@github.com> * Update docs/content/SegmentedControl.mdx Co-authored-by: Cole Bemis <colebemis@github.com> * imports ResponsiveValues type Co-authored-by: Cole Bemis <colebemis@github.com>
1 parent 82fd8c3 commit 7edf134

File tree

7 files changed

+84
-264
lines changed

7 files changed

+84
-264
lines changed

.changeset/bright-timers-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': minor
3+
---
4+
5+
Adds responsive behavior to SegmentedControl's `fullWidth` prop.

docs/content/SegmentedControl.mdx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,16 @@ description: Use a segmented control to let users select an option from a short
151151
<PropsTableRow name="aria-label" type="string" />
152152
<PropsTableRow name="aria-labelledby" type="string" />
153153
<PropsTableRow name="aria-describedby" type="string" />
154-
<PropsTableRow name="fullWidth" type="boolean" description="Whether the control fills the width of its parent" />
154+
<PropsTableRow
155+
name="fullWidth"
156+
type={`| boolean
157+
| {
158+
narrow?: boolean
159+
regular?: boolean
160+
wide?: boolean
161+
}`}
162+
description="Whether the control fills the width of its parent"
163+
/>
155164
<PropsTableRow name="loading" type="boolean" description="Whether the selected segment is being calculated" />
156165
<PropsTableRow
157166
name="onChange"
@@ -161,10 +170,12 @@ description: Use a segmented control to let users select an option from a short
161170
/>
162171
<PropsTableRow
163172
name="variant"
164-
type="'default' | {
165-
narrow?: 'hideLabels' | 'dropdown' | 'default'
166-
regular?: 'hideLabels' | 'dropdown' | 'default'
167-
}"
173+
type={`| 'default'
174+
| {
175+
narrow?: 'hideLabels' | 'dropdown' | 'default'
176+
regular?: 'hideLabels' | 'dropdown' | 'default'
177+
wide?: 'hideLabels' | 'dropdown' | 'default'
178+
}`}
168179
defaultValue="'default'"
169180
description="Configure alternative ways to render the control when it gets rendered in tight spaces"
170181
/>

src/SegmentedControl/SegmentedControl.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {SegmentedControl} from '.' // TODO: update import when we move this to t
99
import theme from '../theme'
1010
import {BaseStyles, SSRProvider, ThemeProvider} from '..'
1111
import {act} from 'react-test-renderer'
12-
import {viewportRanges} from '../hooks/useMatchMedia'
12+
import {viewportRanges} from '../hooks/useResponsiveValue'
1313

1414
const segmentData = [
1515
{label: 'Preview', id: 'preview', iconLabel: 'EyeIcon', icon: () => <EyeIcon aria-label="EyeIcon" />},

src/SegmentedControl/SegmentedControl.tsx

Lines changed: 9 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Button, {SegmentedControlButtonProps} from './SegmentedControlButton'
33
import SegmentedControlIconButton, {SegmentedControlIconButtonProps} from './SegmentedControlIconButton'
44
import {ActionList, ActionMenu, Box, useTheme} from '..'
55
import {merge, SxProp} from '../sx'
6-
import useMatchMedia from '../hooks/useMatchMedia'
6+
import {ResponsiveValue, useResponsiveValue} from '../hooks/useResponsiveValue'
77
import {ViewportRangeKeys} from '../utils/types/ViewportRangeKeys'
88
import {FocusKeys, FocusZoneHookSettings, useFocusZone} from '../hooks/useFocusZone'
99

@@ -14,20 +14,20 @@ type SegmentedControlProps = {
1414
'aria-labelledby'?: string
1515
'aria-describedby'?: string
1616
/** Whether the control fills the width of its parent */
17-
fullWidth?: boolean
17+
fullWidth?: boolean | ResponsiveValue<boolean>
1818
/** The handler that gets called when a segment is selected */
1919
onChange?: (selectedIndex: number) => void
2020
/** Configure alternative ways to render the control when it gets rendered in tight spaces */
2121
variant?: 'default' | Partial<Record<WidthOnlyViewportRangeKeys, 'hideLabels' | 'dropdown' | 'default'>>
2222
} & SxProp
2323

24-
const getSegmentedControlStyles = (props?: SegmentedControlProps) => ({
24+
const getSegmentedControlStyles = (isFullWidth?: boolean) => ({
2525
backgroundColor: 'segmentedControl.bg',
2626
borderColor: 'border.default',
2727
borderRadius: 2,
2828
borderStyle: 'solid',
2929
borderWidth: 1,
30-
display: props?.fullWidth ? 'flex' : 'inline-flex',
30+
display: isFullWidth ? 'flex' : 'inline-flex',
3131
height: '32px' // TODO: use primitive `control.medium.size` when it is available
3232
})
3333

@@ -43,13 +43,8 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
4343
}) => {
4444
const segmentedControlContainerRef = useRef<HTMLSpanElement>(null)
4545
const {theme} = useTheme()
46-
const mediaQueryMatches = useMatchMedia(Object.keys(variant || {}) as WidthOnlyViewportRangeKeys[])
47-
const mediaQueryMatchesKeys = mediaQueryMatches
48-
? (Object.keys(mediaQueryMatches) as WidthOnlyViewportRangeKeys[]).filter(
49-
viewportRangeKey => typeof mediaQueryMatches === 'object' && mediaQueryMatches[viewportRangeKey]
50-
)
51-
: []
52-
46+
const responsiveVariant = useResponsiveValue(variant, 'default')
47+
const isFullWidth = useResponsiveValue(fullWidth, false)
5348
const selectedSegments = React.Children.toArray(children).map(
5449
child =>
5550
React.isValidElement<SegmentedControlButtonProps | SegmentedControlIconButtonProps>(child) && child.props.selected
@@ -79,13 +74,7 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
7974

8075
return React.isValidElement<SegmentedControlIconButtonProps>(childArg) ? childArg.props['aria-label'] : null
8176
}
82-
83-
const sx = merge(
84-
getSegmentedControlStyles({
85-
fullWidth
86-
}),
87-
sxProp as SxProp
88-
)
77+
const sx = merge(getSegmentedControlStyles(isFullWidth), sxProp as SxProp)
8978

9079
const focusInStrategy: FocusZoneHookSettings['focusInStrategy'] = () => {
9180
if (segmentedControlContainerRef.current) {
@@ -113,37 +102,7 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
113102
)
114103
}
115104

116-
// Since we can have multiple media query matches for `variant` (e.g.: 'regular' and 'wide'),
117-
// we need to pick which variant we actually show.
118-
const getVariantToRender = () => {
119-
// If no variant was passed, return 'default'
120-
if (!variant || variant === 'default') {
121-
return 'default'
122-
}
123-
124-
// Prioritize viewport range keys that override the 'regular' range in order of
125-
// priorty from lowest to highest
126-
// Orientation keys beat 'wide' because they are more specific.
127-
const viewportRangeKeysByPriority: ViewportRangeKeys[] = ['wide', 'portrait', 'landscape']
128-
129-
// Filter the viewport range keys to only include those that:
130-
// - are in the priority list
131-
// - have a variant set
132-
const variantPriorityKeys = mediaQueryMatchesKeys.filter(key => {
133-
return viewportRangeKeysByPriority.includes(key) && variant[key]
134-
})
135-
136-
// If we have to pick from multiple variants and one or more of them overrides 'regular',
137-
// use the last key from the filtered list.
138-
if (mediaQueryMatchesKeys.length > 1 && variantPriorityKeys.length) {
139-
return variant[variantPriorityKeys[variantPriorityKeys.length - 1]]
140-
}
141-
142-
// Otherwise, use the variant for the first matching media query
143-
return typeof mediaQueryMatches === 'object' && variant[mediaQueryMatchesKeys[0]]
144-
}
145-
146-
return getVariantToRender() === 'dropdown' ? (
105+
return responsiveVariant === 'dropdown' ? (
147106
// Render the 'dropdown' variant of the SegmentedControlButton or SegmentedControlIconButton
148107
<ActionMenu>
149108
<ActionMenu.Button leadingIcon={getChildIcon(selectedChild)}>{getChildText(selectedChild)}</ActionMenu.Button>
@@ -211,7 +170,7 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
211170

212171
// Render the 'hideLabels' variant of the SegmentedControlButton
213172
if (
214-
getVariantToRender() === 'hideLabels' &&
173+
responsiveVariant === 'hideLabels' &&
215174
React.isValidElement<SegmentedControlButtonProps>(child) &&
216175
child.type === Button
217176
) {

src/SegmentedControl/examples.stories.tsx

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import Box from '../Box'
99
type ResponsiveVariantOptions = 'dropdown' | 'hideLabels' | 'default'
1010
type Args = {
1111
fullWidth?: boolean
12+
fullWidthAtNarrow?: boolean
13+
fullWidthAtRegular?: boolean
14+
fullWidthAtWide?: boolean
1215
variantAtNarrow: ResponsiveVariantOptions
13-
variantAtNarrowLandscape: ResponsiveVariantOptions
1416
variantAtRegular: ResponsiveVariantOptions
1517
variantAtWide: ResponsiveVariantOptions
16-
variantAtPortrait: ResponsiveVariantOptions
17-
variantAtLandscape: ResponsiveVariantOptions
1818
}
1919

2020
const excludedControlKeys = [
@@ -29,24 +29,20 @@ const excludedControlKeys = [
2929

3030
const variantOptions = ['dropdown', 'hideLabels', 'default']
3131

32-
const parseVarientFromArgs = (args: Args) => {
33-
const {
34-
variantAtNarrow,
35-
variantAtNarrowLandscape,
36-
variantAtRegular,
37-
variantAtWide,
38-
variantAtPortrait,
39-
variantAtLandscape
40-
} = args
41-
return {
42-
narrow: variantAtNarrow,
43-
narrowLandscape: variantAtNarrowLandscape,
44-
regular: variantAtRegular,
45-
wide: variantAtWide,
46-
portrait: variantAtPortrait,
47-
landscape: variantAtLandscape
48-
}
49-
}
32+
const parseVariantFromArgs = ({variantAtNarrow, variantAtRegular, variantAtWide}: Args) => ({
33+
narrow: variantAtNarrow,
34+
regular: variantAtRegular,
35+
wide: variantAtWide
36+
})
37+
38+
const parseFullWidthFromArgs = ({fullWidth, fullWidthAtNarrow, fullWidthAtRegular, fullWidthAtWide}: Args) =>
39+
fullWidth
40+
? fullWidth
41+
: {
42+
narrow: fullWidthAtNarrow,
43+
regular: fullWidthAtRegular,
44+
wide: fullWidthAtWide
45+
}
5046

5147
export default {
5248
title: 'SegmentedControl/examples',
@@ -58,48 +54,42 @@ export default {
5854
type: 'boolean'
5955
}
6056
},
61-
variantAtNarrow: {
62-
name: 'variant.narrow',
63-
defaultValue: 'default',
57+
fullWidthAtNarrow: {
58+
defaultValue: false,
6459
control: {
65-
type: 'radio',
66-
options: variantOptions
60+
type: 'boolean'
6761
}
6862
},
69-
variantAtNarrowLandscape: {
70-
name: 'variant.narrowLandscape',
71-
defaultValue: 'default',
63+
fullWidthAtRegular: {
64+
defaultValue: false,
7265
control: {
73-
type: 'radio',
74-
options: variantOptions
66+
type: 'boolean'
7567
}
7668
},
77-
variantAtRegular: {
78-
name: 'variant.regular',
79-
defaultValue: 'default',
69+
fullWidthAtWide: {
70+
defaultValue: false,
8071
control: {
81-
type: 'radio',
82-
options: variantOptions
72+
type: 'boolean'
8373
}
8474
},
85-
variantAtWide: {
86-
name: 'variant.wide',
75+
variantAtNarrow: {
76+
name: 'variant.narrow',
8777
defaultValue: 'default',
8878
control: {
8979
type: 'radio',
9080
options: variantOptions
9181
}
9282
},
93-
variantAtPortrait: {
94-
name: 'variant.portrait',
83+
variantAtRegular: {
84+
name: 'variant.regular',
9585
defaultValue: 'default',
9686
control: {
9787
type: 'radio',
9888
options: variantOptions
9989
}
10090
},
101-
variantAtLandscape: {
102-
name: 'variant.Landscape',
91+
variantAtWide: {
92+
name: 'variant.wide',
10393
defaultValue: 'default',
10494
control: {
10595
type: 'radio',
@@ -122,7 +112,11 @@ export default {
122112
} as Meta
123113

124114
export const Default = (args: Args) => (
125-
<SegmentedControl aria-label="File view" {...args} variant={parseVarientFromArgs(args)}>
115+
<SegmentedControl
116+
aria-label="File view"
117+
fullWidth={parseFullWidthFromArgs(args)}
118+
variant={parseVariantFromArgs(args)}
119+
>
126120
<SegmentedControl.Button selected>Preview</SegmentedControl.Button>
127121
<SegmentedControl.Button>Raw</SegmentedControl.Button>
128122
<SegmentedControl.Button>Blame</SegmentedControl.Button>
@@ -136,7 +130,12 @@ export const Controlled = (args: Args) => {
136130
}
137131

138132
return (
139-
<SegmentedControl aria-label="File view" onChange={handleChange} {...args} variant={parseVarientFromArgs(args)}>
133+
<SegmentedControl
134+
aria-label="File view"
135+
onChange={handleChange}
136+
fullWidth={parseFullWidthFromArgs(args)}
137+
variant={parseVariantFromArgs(args)}
138+
>
140139
<SegmentedControl.Button selected={selectedIndex === 0}>Preview</SegmentedControl.Button>
141140
<SegmentedControl.Button selected={selectedIndex === 1}>Raw</SegmentedControl.Button>
142141
<SegmentedControl.Button selected={selectedIndex === 2}>Blame</SegmentedControl.Button>
@@ -148,7 +147,11 @@ export const WithIconsAndLabels = (args: Args) => (
148147
// padding needed to show Tooltip
149148
// there is a separate initiative to change Tooltip to get positioned with useAnchoredPosition
150149
<Box pt={5}>
151-
<SegmentedControl aria-label="File view" {...args} variant={parseVarientFromArgs(args)}>
150+
<SegmentedControl
151+
aria-label="File view"
152+
fullWidth={parseFullWidthFromArgs(args)}
153+
variant={parseVariantFromArgs(args)}
154+
>
152155
<SegmentedControl.Button selected leadingIcon={EyeIcon}>
153156
Preview
154157
</SegmentedControl.Button>
@@ -162,7 +165,11 @@ export const IconsOnly = (args: Args) => (
162165
// padding needed to show Tooltip
163166
// there is a separate initiative to change Tooltip to get positioned with useAnchoredPosition
164167
<Box pt={5}>
165-
<SegmentedControl aria-label="File view" {...args} variant={parseVarientFromArgs(args)}>
168+
<SegmentedControl
169+
aria-label="File view"
170+
fullWidth={parseFullWidthFromArgs(args)}
171+
variant={parseVariantFromArgs(args)}
172+
>
166173
<SegmentedControl.IconButton selected icon={EyeIcon} aria-label="Preview" />
167174
<SegmentedControl.IconButton icon={FileCodeIcon} aria-label="Raw" />
168175
<SegmentedControl.IconButton icon={PeopleIcon} aria-label="Blame" />

src/__tests__/hooks/useMatchMedia.test.tsx

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)