Skip to content

SegmentedControl variant prop #2164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7710793
renders a tooltip for icon-only segmented control buttons
mperrotti Jun 28, 2022
9927ea5
implements responsive variant prop
mperrotti Jun 29, 2022
566af62
adds tests
mperrotti Jun 30, 2022
4c394de
minor story tweaks
mperrotti Jun 30, 2022
152293a
refactor useMatchMedia
mperrotti Jul 1, 2022
63f9eb8
adds useMatchMedia tests, fixes useMatchMedia bugs, updates Segmented…
mperrotti Jul 1, 2022
cc1d998
removes aria attributes from Storybook controls
mperrotti Jul 1, 2022
6f213cc
rm irrelevant 'TODO' comments
mperrotti Jul 1, 2022
1f924b5
Merge branch 'main' of github.com:primer/react into mp/segmented-cont…
mperrotti Jul 1, 2022
97bc927
adds changeset
mperrotti Jul 1, 2022
ff0ffa2
adds helpful comments
mperrotti Jul 18, 2022
cb65727
fixes button font-size in Safari
mperrotti Jul 18, 2022
2be3b89
updates snapshots
mperrotti Jul 18, 2022
8086767
addresses PR feedback
mperrotti Jul 19, 2022
333e9bf
Merge branch 'main' into mp/segmented-control-variant-prop
mperrotti Jul 20, 2022
d15f987
Merge branch 'main' into mp/segmented-control-variant-prop
mperrotti Jul 21, 2022
2efe8b0
Update docs/content/SegmentedControl.mdx
mperrotti Jul 21, 2022
8bc3e73
Update docs/content/SegmentedControl.mdx
mperrotti Jul 21, 2022
b1c7a76
Update .changeset/pretty-students-judge.md
mperrotti Jul 21, 2022
67c6df6
bumps @primer/primitives to version with segmented control variables
mperrotti Jul 23, 2022
d524e07
Merge branch 'main' of github.com:primer/react into mp/segmented-cont…
mperrotti Jul 25, 2022
e881f0f
corrects storybook knobs to match current API
mperrotti Jul 25, 2022
3cb68f8
Merge branch 'mp/segmented-control-variant-prop' of github.com:primer…
mperrotti Jul 25, 2022
afabbee
rm 'wide' key from 'variant' prop in props table
mperrotti Jul 25, 2022
b3d1e82
fix bad merge in SegmentedControl
mperrotti Jul 25, 2022
891e357
adds more context to a11y issues with the tooltip implementation
mperrotti Jul 25, 2022
104b6de
adds changeset
mperrotti Jul 25, 2022
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/pretty-students-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Adds support for a responsive 'variant' prop to the SegmentedControl component
12 changes: 6 additions & 6 deletions docs/content/SegmentedControl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ description: Use a segmented control to let users select an option from a short
### With labels hidden on smaller viewports

```jsx live drafts
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels', regular: 'none'}}>
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels', regular: 'default'}}>
Copy link
Contributor

Choose a reason for hiding this comment

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

non-blocking: hideLabels looks like a boolean type as opposed to dropdown variant. Wouldn't it be more consistent to use icons instead?

<SegmentedControl.Button selected leadingIcon={EyeIcon}>
Preview
</SegmentedControl.Button>
Expand All @@ -55,7 +55,7 @@ description: Use a segmented control to let users select an option from a short
### Convert to a dropdown on smaller viewports

```jsx live drafts
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown', regular: 'none'}}>
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown', regular: 'default'}}>
<SegmentedControl.Button selected leadingIcon={EyeIcon}>
Preview
</SegmentedControl.Button>
Expand Down Expand Up @@ -161,11 +161,11 @@ description: Use a segmented control to let users select an option from a short
/>
<PropsTableRow
name="variant"
type="{
narrow?: 'hideLabels' | 'dropdown',
regular?: 'hideLabels' | 'dropdown',
wide?: 'hideLabels' | 'dropdown'
type="'default' | {
narrow?: 'hideLabels' | 'dropdown' | 'default'
regular?: 'hideLabels' | 'dropdown' | 'default'
}"
defaultValue="'default'"
description="Configure alternative ways to render the control when it gets rendered in tight spaces"
/>
<PropsTableSxRow />
Expand Down
24 changes: 22 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"jest": "27.4.5",
"jest-axe": "5.0.1",
"jest-styled-components": "6.3.4",
"jest-matchmedia-mock": "1.1.0",
"jscodeshift": "0.13.0",
"lint-staged": "12.1.2",
"lodash.isempty": "4.4.0",
Expand Down
137 changes: 134 additions & 3 deletions src/SegmentedControl/SegmentedControl.test.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import {fireEvent, render} from '@testing-library/react'
import MatchMediaMock from 'jest-matchmedia-mock'
import {render, fireEvent, waitFor} from '@testing-library/react'
import {EyeIcon, FileCodeIcon, PeopleIcon} from '@primer/octicons-react'
import userEvent from '@testing-library/user-event'
import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
import {SegmentedControl} from '.' // TODO: update import when we move this to the global index
import theme from '../theme'
import {BaseStyles, SSRProvider, ThemeProvider} from '..'
import {act} from 'react-test-renderer'
import {viewportRanges} from '../hooks/useMatchMedia'

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

// TODO: improve test coverage
let matchMedia: MatchMediaMock

describe('SegmentedControl', () => {
const mockWarningFn = jest.fn()

beforeAll(() => {
jest.spyOn(global.console, 'warn').mockImplementation(mockWarningFn)
matchMedia = new MatchMediaMock()
})

afterAll(() => {
jest.clearAllMocks()
matchMedia.clear()
})

behavesAsComponent({
Expand Down Expand Up @@ -54,6 +66,47 @@ describe('SegmentedControl', () => {
expect(selectedButton?.getAttribute('aria-current')).toBe('true')
})

it('renders the dropdown variant', () => {
act(() => {
matchMedia.useMediaQuery(viewportRanges.narrow)
})

const {getByText} = render(
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown'}}>
{segmentData.map(({label}, index) => (
<SegmentedControl.Button selected={index === 1} key={label}>
{label}
</SegmentedControl.Button>
))}
</SegmentedControl>
)
const button = getByText(segmentData[1].label)

expect(button).toBeInTheDocument()
expect(button.closest('button')?.getAttribute('aria-haspopup')).toBe('true')
})

it('renders the hideLabels variant', () => {
act(() => {
matchMedia.useMediaQuery(viewportRanges.narrow)
})

const {getByLabelText} = render(
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels'}}>
{segmentData.map(({label, icon}, index) => (
<SegmentedControl.Button leadingIcon={icon} selected={index === 1} key={label}>
{label}
</SegmentedControl.Button>
))}
</SegmentedControl>
)

for (const datum of segmentData) {
const labelledButton = getByLabelText(datum.label)
expect(labelledButton).toBeDefined()
}
})

it('renders the first segment as selected if no child has the `selected` prop passed', () => {
const {getByText} = render(
<SegmentedControl aria-label="File view">
Expand Down Expand Up @@ -190,6 +243,83 @@ describe('SegmentedControl', () => {
expect(document.activeElement?.id).toEqual(initialFocusButtonNode.id)
})

it('calls onChange with index of clicked segment button when using the dropdown variant', async () => {
act(() => {
matchMedia.useMediaQuery(viewportRanges.narrow)
})
const handleChange = jest.fn()
const component = render(
<ThemeProvider theme={theme}>
<SSRProvider>
<BaseStyles>
<SegmentedControl aria-label="File view" onChange={handleChange} variant={{narrow: 'dropdown'}}>
{segmentData.map(({label}, index) => (
<SegmentedControl.Button selected={index === 0} key={label}>
{label}
</SegmentedControl.Button>
))}
</SegmentedControl>
</BaseStyles>
</SSRProvider>
</ThemeProvider>
)
const button = component.getByText(segmentData[0].label)

fireEvent.click(button)
expect(handleChange).not.toHaveBeenCalled()
const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
fireEvent.click(menuItems[1])

expect(handleChange).toHaveBeenCalledWith(1)
})

it('calls segment button onClick if it is passed when using the dropdown variant', async () => {
act(() => {
matchMedia.useMediaQuery(viewportRanges.narrow)
})
const handleClick = jest.fn()
const component = render(
<ThemeProvider theme={theme}>
<SSRProvider>
<BaseStyles>
<SegmentedControl aria-label="File view" variant={{narrow: 'dropdown'}}>
{segmentData.map(({label}, index) => (
<SegmentedControl.Button selected={index === 0} key={label} onClick={handleClick}>
{label}
</SegmentedControl.Button>
))}
</SegmentedControl>
</BaseStyles>
</SSRProvider>
</ThemeProvider>
)
const button = component.getByText(segmentData[0].label)

fireEvent.click(button)
expect(handleClick).not.toHaveBeenCalled()
const menuItems = await waitFor(() => component.getAllByRole('menuitemradio'))
fireEvent.click(menuItems[1])

expect(handleClick).toHaveBeenCalled()
})

it('warns users if they try to use the hideLabels variant without a leadingIcon', () => {
act(() => {
matchMedia.useMediaQuery(viewportRanges.narrow)
})
const consoleSpy = jest.spyOn(global.console, 'warn')
render(
<SegmentedControl aria-label="File view" variant={{narrow: 'hideLabels'}}>
{segmentData.map(({label}, index) => (
<SegmentedControl.Button selected={index === 1} key={label}>
{label}
</SegmentedControl.Button>
))}
</SegmentedControl>
)
expect(consoleSpy).toHaveBeenCalled()
})

it('should warn the user if they neglect to specify a label for the segmented control', () => {
render(
<SegmentedControl>
Expand All @@ -205,5 +335,6 @@ describe('SegmentedControl', () => {
})
})

checkStoriesForAxeViolations('examples', '../SegmentedControl/')
// TODO: uncomment these tests after we fix a11y for the Tooltip component
// checkStoriesForAxeViolations('examples', '../SegmentedControl/')
checkStoriesForAxeViolations('fixtures', '../SegmentedControl/')
Loading