Skip to content

Commit 019d85a

Browse files
committed
Merge branch 'main' of github.com:primer/react into mp/segmented-control-use-component-primitives
2 parents 9df05d2 + 2b5c86e commit 019d85a

15 files changed

+682
-89
lines changed
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 support for a responsive 'variant' prop to the SegmentedControl component

docs/content/SegmentedControl.mdx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ description: Use a segmented control to let users select an option from a short
4343
### With labels hidden on smaller viewports
4444

4545
```jsx live drafts
46-
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels', regular: 'none'}}>
46+
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels', regular: 'default'}}>
4747
<SegmentedControl.Button selected leadingIcon={EyeIcon}>
4848
Preview
4949
</SegmentedControl.Button>
@@ -55,7 +55,7 @@ description: Use a segmented control to let users select an option from a short
5555
### Convert to a dropdown on smaller viewports
5656

5757
```jsx live drafts
58-
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown', regular: 'none'}}>
58+
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown', regular: 'default'}}>
5959
<SegmentedControl.Button selected leadingIcon={EyeIcon}>
6060
Preview
6161
</SegmentedControl.Button>
@@ -161,11 +161,11 @@ description: Use a segmented control to let users select an option from a short
161161
/>
162162
<PropsTableRow
163163
name="variant"
164-
type="{
165-
narrow?: 'hideLabels' | 'dropdown',
166-
regular?: 'hideLabels' | 'dropdown',
167-
wide?: 'hideLabels' | 'dropdown'
164+
type="'default' | {
165+
narrow?: 'hideLabels' | 'dropdown' | 'default'
166+
regular?: 'hideLabels' | 'dropdown' | 'default'
168167
}"
168+
defaultValue="'default'"
169169
description="Configure alternative ways to render the control when it gets rendered in tight spaces"
170170
/>
171171
<PropsTableSxRow />

package-lock.json

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
"jest": "27.4.5",
164164
"jest-axe": "5.0.1",
165165
"jest-styled-components": "6.3.4",
166+
"jest-matchmedia-mock": "1.1.0",
166167
"jscodeshift": "0.13.0",
167168
"lint-staged": "12.1.2",
168169
"lodash.isempty": "4.4.0",

src/SegmentedControl/SegmentedControl.test.tsx

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
import React from 'react'
22
import '@testing-library/jest-dom/extend-expect'
3-
import {fireEvent, render} from '@testing-library/react'
3+
import MatchMediaMock from 'jest-matchmedia-mock'
4+
import {render, fireEvent, waitFor} from '@testing-library/react'
45
import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react'
56
import userEvent from '@testing-library/user-event'
67
import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
78
import {SegmentedControl} from '.' // TODO: update import when we move this to the global index
9+
import theme from '../theme'
10+
import {BaseStyles, SSRProvider, ThemeProvider} from '..'
11+
import {act} from 'react-test-renderer'
12+
import {viewportRanges} from '../hooks/useMatchMedia'
813

914
const segmentData = [
1015
{label: 'Preview', id: 'preview', iconLabel: 'EyeIcon', icon: () => <EyeIcon aria-label="EyeIcon" />},
1116
{label: 'Raw', id: 'raw', iconLabel: 'FileCodeIcon', icon: () => <FileCodeIcon aria-label="FileCodeIcon" />},
1217
{label: 'Blame', id: 'blame', iconLabel: 'PeopleIcon', icon: () => <PeopleIcon aria-label="PeopleIcon" />}
1318
]
1419

15-
// TODO: improve test coverage
20+
let matchMedia: MatchMediaMock
21+
1622
describe('SegmentedControl', () => {
1723
const mockWarningFn = jest.fn()
1824

1925
beforeAll(() => {
2026
jest.spyOn(global.console, 'warn').mockImplementation(mockWarningFn)
27+
matchMedia = new MatchMediaMock()
28+
})
29+
30+
afterAll(() => {
31+
jest.clearAllMocks()
32+
matchMedia.clear()
2133
})
2234

2335
behavesAsComponent({
@@ -54,6 +66,47 @@ describe('SegmentedControl', () => {
5466
expect(selectedButton?.getAttribute('aria-current')).toBe('true')
5567
})
5668

69+
it('renders the dropdown variant', () => {
70+
act(() => {
71+
matchMedia.useMediaQuery(viewportRanges.narrow)
72+
})
73+
74+
const {getByText} = render(
75+
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown'}}>
76+
{segmentData.map(({label}, index) => (
77+
<SegmentedControl.Button selected={index === 1} key={label}>
78+
{label}
79+
</SegmentedControl.Button>
80+
))}
81+
</SegmentedControl>
82+
)
83+
const button = getByText(segmentData[1].label)
84+
85+
expect(button).toBeInTheDocument()
86+
expect(button.closest('button')?.getAttribute('aria-haspopup')).toBe('true')
87+
})
88+
89+
it('renders the hideLabels variant', () => {
90+
act(() => {
91+
matchMedia.useMediaQuery(viewportRanges.narrow)
92+
})
93+
94+
const {getByLabelText} = render(
95+
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels'}}>
96+
{segmentData.map(({label, icon}, index) => (
97+
<SegmentedControl.Button leadingIcon={icon} selected={index === 1} key={label}>
98+
{label}
99+
</SegmentedControl.Button>
100+
))}
101+
</SegmentedControl>
102+
)
103+
104+
for (const datum of segmentData) {
105+
const labelledButton = getByLabelText(datum.label)
106+
expect(labelledButton).toBeDefined()
107+
}
108+
})
109+
57110
it('renders the first segment as selected if no child has the `selected` prop passed', () => {
58111
const {getByText} = render(
59112
<SegmentedControl aria-label="File view">
@@ -190,6 +243,83 @@ describe('SegmentedControl', () => {
190243
expect(document.activeElement?.id).toEqual(initialFocusButtonNode.id)
191244
})
192245

246+
it('calls onChange with index of clicked segment button when using the dropdown variant', async () => {
247+
act(() => {
248+
matchMedia.useMediaQuery(viewportRanges.narrow)
249+
})
250+
const handleChange = jest.fn()
251+
const component = render(
252+
<ThemeProvider theme={theme}>
253+
<SSRProvider>
254+
<BaseStyles>
255+
<SegmentedControl aria-label="File view" onChange={handleChange} variant={{narrow: 'dropdown'}}>
256+
{segmentData.map(({label}, index) => (
257+
<SegmentedControl.Button selected={index === 0} key={label}>
258+
{label}
259+
</SegmentedControl.Button>
260+
))}
261+
</SegmentedControl>
262+
</BaseStyles>
263+
</SSRProvider>
264+
</ThemeProvider>
265+
)
266+
const button = component.getByText(segmentData[0].label)
267+
268+
fireEvent.click(button)
269+
expect(handleChange).not.toHaveBeenCalled()
270+
const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
271+
fireEvent.click(menuItems[1])
272+
273+
expect(handleChange).toHaveBeenCalledWith(1)
274+
})
275+
276+
it('calls segment button onClick if it is passed when using the dropdown variant', async () => {
277+
act(() => {
278+
matchMedia.useMediaQuery(viewportRanges.narrow)
279+
})
280+
const handleClick = jest.fn()
281+
const component = render(
282+
<ThemeProvider theme={theme}>
283+
<SSRProvider>
284+
<BaseStyles>
285+
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown'}}>
286+
{segmentData.map(({label}, index) => (
287+
<SegmentedControl.Button selected={index === 0} key={label} onClick={handleClick}>
288+
{label}
289+
</SegmentedControl.Button>
290+
))}
291+
</SegmentedControl>
292+
</BaseStyles>
293+
</SSRProvider>
294+
</ThemeProvider>
295+
)
296+
const button = component.getByText(segmentData[0].label)
297+
298+
fireEvent.click(button)
299+
expect(handleClick).not.toHaveBeenCalled()
300+
const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
301+
fireEvent.click(menuItems[1])
302+
303+
expect(handleClick).toHaveBeenCalled()
304+
})
305+
306+
it('warns users if they try to use the hideLabels variant without a leadingIcon', () => {
307+
act(() => {
308+
matchMedia.useMediaQuery(viewportRanges.narrow)
309+
})
310+
const consoleSpy = jest.spyOn(global.console, 'warn')
311+
render(
312+
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels'}}>
313+
{segmentData.map(({label}, index) => (
314+
<SegmentedControl.Button selected={index === 1} key={label}>
315+
{label}
316+
</SegmentedControl.Button>
317+
))}
318+
</SegmentedControl>
319+
)
320+
expect(consoleSpy).toHaveBeenCalled()
321+
})
322+
193323
it('should warn the user if they neglect to specify a label for the segmented control', () => {
194324
render(
195325
<SegmentedControl>
@@ -205,5 +335,6 @@ describe('SegmentedControl', () => {
205335
})
206336
})
207337

208-
checkStoriesForAxeViolations('examples', '../SegmentedControl/')
338+
// TODO: uncomment these tests after we fix a11y for the Tooltip component
339+
// checkStoriesForAxeViolations('examples', '../SegmentedControl/')
209340
checkStoriesForAxeViolations('fixtures', '../SegmentedControl/')

0 commit comments

Comments
 (0)