Skip to content

Commit 73312d8

Browse files
TylerJDevjoshblack
andauthored
ProgressBar: Adjust ProgressBar.Item for accessibility (#4878)
* Adjust `ProgressBar.Item` for accessibility * Move warning * Include if progressAsNumber === 0 * Update snapshots, tests, move aria-* attributes * Update warning * Update `ProgressBar.Item` props * Account for `0` * Add changeset * Change children conditional * Remove warning, add default 0 * Fix lint, test * Update packages/react/src/ProgressBar/ProgressBar.tsx Co-authored-by: Josh Black <[email protected]> * Update packages/react/src/ProgressBar/ProgressBar.tsx Co-authored-by: Josh Black <[email protected]> * Update packages/react/src/ProgressBar/ProgressBar.tsx Co-authored-by: Josh Black <[email protected]> --------- Co-authored-by: Josh Black <[email protected]>
1 parent a5d7fe3 commit 73312d8

File tree

4 files changed

+117
-29
lines changed

4 files changed

+117
-29
lines changed

.changeset/lucky-horses-kiss.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+
Move `aria-*` attributes to `ProgressBar.Item` and marks `ProgressBar.Item` as `role="progressbar".

packages/react/src/ProgressBar/ProgressBar.tsx

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {width} from 'styled-system'
55
import {get} from '../constants'
66
import type {SxProp} from '../sx'
77
import sx from '../sx'
8-
import {warning} from '../utils/warning'
98

109
type ProgressProp = {
1110
progress?: string | number
@@ -17,7 +16,7 @@ const shimmer = keyframes`
1716
to { mask-position: 0%; }
1817
`
1918

20-
export const Item = styled.span<ProgressProp & SxProp>`
19+
const ProgressItem = styled.span<ProgressProp & SxProp>`
2120
width: ${props => (props.progress ? `${props.progress}%` : 0)};
2221
background-color: ${props => get(`colors.${props.bg || 'success.emphasis'}`)};
2322
@@ -34,8 +33,6 @@ export const Item = styled.span<ProgressProp & SxProp>`
3433
${sx};
3534
`
3635

37-
Item.displayName = 'ProgressBar.Item'
38-
3936
const sizeMap = {
4037
small: '5px',
4138
large: '10px',
@@ -60,37 +57,78 @@ const ProgressContainer = styled.span<StyledProgressContainerProps>`
6057
${sx};
6158
`
6259

60+
export type ProgressBarItems = React.HTMLAttributes<HTMLSpanElement> & {'aria-label'?: string} & ProgressProp & SxProp
61+
62+
export const Item = forwardRef<HTMLSpanElement, ProgressBarItems>(
63+
(
64+
{progress, 'aria-label': ariaLabel, 'aria-valuenow': ariaValueNow, 'aria-valuetext': ariaValueText, ...rest},
65+
forwardRef,
66+
) => {
67+
const progressAsNumber = typeof progress === 'string' ? parseInt(progress, 10) : progress
68+
69+
const ariaAttributes = {
70+
'aria-valuenow':
71+
ariaValueNow ?? (progressAsNumber !== undefined && progressAsNumber >= 0 ? Math.round(progressAsNumber) : 0),
72+
'aria-valuemin': 0,
73+
'aria-valuemax': 100,
74+
'aria-valuetext': ariaValueText,
75+
}
76+
77+
return (
78+
<ProgressItem
79+
{...rest}
80+
role="progressbar"
81+
aria-label={ariaLabel}
82+
ref={forwardRef}
83+
progress={progress}
84+
{...ariaAttributes}
85+
/>
86+
)
87+
},
88+
)
89+
90+
Item.displayName = 'ProgressBar.Item'
91+
6392
export type ProgressBarProps = React.HTMLAttributes<HTMLSpanElement> & {bg?: string} & StyledProgressContainerProps &
6493
ProgressProp
6594

6695
export const ProgressBar = forwardRef<HTMLSpanElement, ProgressBarProps>(
6796
(
68-
{animated, progress, bg = 'success.emphasis', barSize = 'default', children, ...rest}: ProgressBarProps,
97+
{
98+
animated,
99+
progress,
100+
bg = 'success.emphasis',
101+
barSize = 'default',
102+
children,
103+
'aria-label': ariaLabel,
104+
'aria-valuenow': ariaValueNow,
105+
'aria-valuetext': ariaValueText,
106+
...rest
107+
}: ProgressBarProps,
69108
forwardRef,
70109
) => {
71110
if (children && progress) {
72111
throw new Error('You should pass `progress` or children, not both.')
73112
}
74113

75-
warning(
76-
children &&
77-
typeof (rest as React.AriaAttributes)['aria-valuenow'] === 'undefined' &&
78-
typeof (rest as React.AriaAttributes)['aria-valuetext'] === 'undefined',
79-
'Expected `aria-valuenow` or `aria-valuetext` to be provided to <ProgressBar>. Provide one of these values so screen reader users can determine the current progress. This warning will become an error in the next major release.',
80-
)
81-
82-
const progressAsNumber = typeof progress === 'string' ? parseInt(progress, 10) : progress
83-
84-
const ariaAttributes = {
85-
'aria-valuenow': progressAsNumber ? Math.round(progressAsNumber) : undefined,
86-
'aria-valuemin': 0,
87-
'aria-valuemax': 100,
88-
'aria-valuetext': progressAsNumber ? `${Math.round(progressAsNumber)}%` : undefined,
89-
}
114+
// Get the number of non-empty nodes passed as children, this will exclude
115+
// booleans, null, and undefined
116+
const validChildren = React.Children.toArray(children).length
90117

91118
return (
92-
<ProgressContainer ref={forwardRef} role="progressbar" barSize={barSize} {...ariaAttributes} {...rest}>
93-
{children ?? <Item data-animated={animated} progress={progress} bg={bg} />}
119+
<ProgressContainer ref={forwardRef} barSize={barSize} {...rest}>
120+
{validChildren ? (
121+
children
122+
) : (
123+
<Item
124+
data-animated={animated}
125+
progress={progress}
126+
aria-label={ariaLabel}
127+
aria-valuenow={ariaValueNow}
128+
aria-valuetext={ariaValueText}
129+
bg={bg}
130+
/>
131+
)}
94132
</ProgressContainer>
95133
)
96134
},

packages/react/src/__tests__/ProgressBar.test.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import axe from 'axe-core'
66
import {FeatureFlags} from '../FeatureFlags'
77

88
describe('ProgressBar', () => {
9-
behavesAsComponent({Component: ProgressBar})
9+
behavesAsComponent({Component: ProgressBar, toRender: () => <ProgressBar aria-valuenow={10} progress={0} />})
1010

1111
checkExports('ProgressBar', {
1212
default: undefined,
@@ -72,4 +72,50 @@ describe('ProgressBar', () => {
7272
it('respects the "progress" prop', () => {
7373
expect(render(<ProgressBar progress={80} aria-label="Upload test.png" />)).toMatchSnapshot()
7474
})
75+
76+
it('passed the `aria-label` down to the progress bar', () => {
77+
const {getByRole, getByLabelText} = HTMLRender(<ProgressBar progress={80} aria-label="Upload test.png" />)
78+
expect(getByRole('progressbar')).toHaveAttribute('aria-label', 'Upload test.png')
79+
expect(getByLabelText('Upload test.png')).toBeInTheDocument()
80+
})
81+
82+
it('passed the `aria-valuenow` down to the progress bar', () => {
83+
const {getByRole} = HTMLRender(<ProgressBar progress={80} aria-valuenow={80} />)
84+
expect(getByRole('progressbar')).toHaveAttribute('aria-valuenow', '80')
85+
})
86+
87+
it('passed the `aria-valuetext` down to the progress bar', () => {
88+
const {getByRole} = HTMLRender(<ProgressBar aria-valuetext="80 percent" />)
89+
expect(getByRole('progressbar')).toHaveAttribute('aria-valuetext', '80 percent')
90+
})
91+
92+
it('does not pass the `aria-label` down to the progress bar if there are multiple items', () => {
93+
const {getByRole} = HTMLRender(
94+
<ProgressBar aria-label="Upload test.png">
95+
<ProgressBar.Item progress={80} />
96+
</ProgressBar>,
97+
)
98+
expect(getByRole('progressbar')).not.toHaveAttribute('aria-label')
99+
})
100+
101+
it('passes aria attributes to the progress bar item', () => {
102+
const {getByRole} = HTMLRender(
103+
<ProgressBar>
104+
<ProgressBar.Item progress={50} aria-label="Progress" ria-valuenow="50"></ProgressBar.Item>
105+
</ProgressBar>,
106+
)
107+
expect(getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50')
108+
expect(getByRole('progressbar')).toHaveAttribute('aria-label', 'Progress')
109+
})
110+
111+
it('provides `aria-valuenow` to the progress bar item if it is not already provided', () => {
112+
const {getByRole} = HTMLRender(<ProgressBar progress={50} />)
113+
expect(getByRole('progressbar')).toHaveAttribute('aria-valuenow', '50')
114+
})
115+
116+
it('applies `0` as a value for `aria-valuenow`', () => {
117+
const {getByRole} = HTMLRender(<ProgressBar progress={0} aria-valuenow={0} aria-label="Upload text.png" />)
118+
119+
expect(getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0')
120+
})
75121
})

packages/react/src/__tests__/__snapshots__/ProgressBar.test.tsx.snap

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,15 @@ exports[`ProgressBar respects the "progress" prop 1`] = `
3434
}
3535
3636
<span
37-
aria-label="Upload test.png"
38-
aria-valuemax={100}
39-
aria-valuemin={0}
40-
aria-valuenow={80}
41-
aria-valuetext="80%"
4237
className="c0"
43-
role="progressbar"
4438
>
4539
<span
40+
aria-label="Upload test.png"
41+
aria-valuemax={100}
42+
aria-valuemin={0}
43+
aria-valuenow={80}
4644
className="c1"
45+
role="progressbar"
4746
/>
4847
</span>
4948
`;

0 commit comments

Comments
 (0)