Skip to content

Commit

Permalink
SegmentedControl variant prop (primer#2164)
Browse files Browse the repository at this point in the history
* renders a tooltip for icon-only segmented control buttons

* implements responsive variant prop

* adds tests

* minor story tweaks

* refactor useMatchMedia

* adds useMatchMedia tests, fixes useMatchMedia bugs, updates SegmentedControl tests

* removes aria attributes from Storybook controls

* rm irrelevant 'TODO' comments

* adds changeset

* adds helpful comments

* fixes button font-size in Safari

* updates snapshots

* addresses PR feedback

* 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>

* Update .changeset/pretty-students-judge.md

Co-authored-by: Josep Martins <jsp.mrtns@gmail.com>

* bumps @primer/primitives to version with segmented control variables

* corrects storybook knobs to match current API

* rm 'wide' key from 'variant' prop in props table

* fix bad merge in SegmentedControl

* adds more context to a11y issues with the tooltip implementation

* adds changeset

Co-authored-by: Cole Bemis <colebemis@github.com>
Co-authored-by: Josep Martins <jsp.mrtns@gmail.com>
  • Loading branch information
3 people authored and PrinceSumberia committed Aug 1, 2022
1 parent e2f385a commit c7903d7
Show file tree
Hide file tree
Showing 15 changed files with 684 additions and 91 deletions.
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'}}>
<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

0 comments on commit c7903d7

Please sign in to comment.