Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/plenty-mangos-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@primer/react': patch
---

Adding aria-attributes and role to the ProgressBar component

<!-- Changed components: ProgressBar -->
6 changes: 3 additions & 3 deletions docs/content/ProgressBar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {ProgressBar} from '@primer/react'
## Examples

```jsx live
<ProgressBar progress={80} />
<ProgressBar progress={80} aria-label="Upload test.png" />
```

### Inline
Expand All @@ -24,7 +24,7 @@ If you'd like to use ProgressBar inline, pass the `inline` boolean prop & **be s
```jsx live
<>
<Text mr={3}>5 of 10</Text>
<ProgressBar progress={50} inline sx={{width: '100px'}} />
<ProgressBar progress={50} inline sx={{width: '100px'}} aria-label="Upload test.png" />
</>
```

Expand All @@ -34,7 +34,7 @@ If you want to show multiple segments in a ProgressBar, pass separate `Item`s as

```jsx live
<>
<ProgressBar>
<ProgressBar aria-label="Upload test.png">
<ProgressBar.Item progress={33} />
<ProgressBar.Item progress={33} sx={{backgroundColor: 'danger.emphasis'}} />
</ProgressBar>
Expand Down
20 changes: 10 additions & 10 deletions src/ProgressBar/ProgressBar.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import React from 'react'
import {ComponentMeta} from '@storybook/react'
import {Meta} from '@storybook/react'
import {ProgressBar} from '..'

export default {
title: 'Components/ProgressBar/Features',
component: ProgressBar,
} as ComponentMeta<typeof ProgressBar>
} as Meta<typeof ProgressBar>

export const ProgressZero = () => <ProgressBar progress="0" />
export const ProgressHalf = () => <ProgressBar progress="50" />
export const ProgressDone = () => <ProgressBar progress="100" />
export const ProgressZero = () => <ProgressBar progress="0" aria-label="Upload test.png" />
export const ProgressHalf = () => <ProgressBar progress="50" aria-label="Upload test.png" />
export const ProgressDone = () => <ProgressBar progress="100" aria-label="Upload test.png" />

export const SizeSmall = () => <ProgressBar progress="66" barSize="small" />
export const SizeLarge = () => <ProgressBar progress="66" barSize="large" />
export const SizeSmall = () => <ProgressBar progress="66" barSize="small" aria-label="Upload test.png" />
export const SizeLarge = () => <ProgressBar progress="66" barSize="large" aria-label="Upload test.png" />

export const Inline = () => <ProgressBar inline progress="66" sx={{width: '100px'}} />
export const Inline = () => <ProgressBar inline progress="66" sx={{width: '100px'}} aria-label="Upload test.png" />

export const Color = () => <ProgressBar progress="66" bg="done.emphasis" />
export const Color = () => <ProgressBar progress="66" bg="done.emphasis" aria-label="Upload test.png" />

export const MultipleItems = () => (
<ProgressBar>
<ProgressBar aria-valuenow={70} aria-label="Upload test.png">
<ProgressBar.Item progress={33} />
<ProgressBar.Item progress={23} sx={{backgroundColor: 'danger.emphasis'}} />
<ProgressBar.Item progress={14} sx={{backgroundColor: 'severe.emphasis'}} />
Expand Down
4 changes: 2 additions & 2 deletions src/ProgressBar/ProgressBar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export default {
component: ProgressBar,
} as Meta<typeof ProgressBar>

export const Default = () => <ProgressBar />
export const Default = () => <ProgressBar aria-label="Upload test.png" />

export const Playground: ComponentStory<typeof ProgressBar> = args => (
<ProgressBar {...args} sx={args.inline ? {width: '100px'} : {}} />
<ProgressBar {...args} sx={args.inline ? {width: '100px'} : {}} aria-label="Upload test.png" />
)

Playground.args = {
Expand Down
56 changes: 36 additions & 20 deletions src/ProgressBar/ProgressBar.tsx
Copy link
Member

Choose a reason for hiding this comment

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

We also have ProgressBar with multiple items like here and we don't get aria attributes in these I realised. There is no usage of ProgressBar.Item though at dotcom so not sure if it would worth the effort. What do you think??

Copy link
Contributor Author

@kendallgassner kendallgassner Jul 14, 2023

Choose a reason for hiding this comment

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

Good catch!!

To deal with multiple I now throw an error if children are passed in without aria-valuenow or 'aria-valuetext:

    throw new Error(
      'Use the `aria-valuenow` or `aria-valuetext` so screen reader users can determine the `progress`.',
    )

What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Oh I see! I have a different take on this but I am not 100% sure, let me know what you think!

Seems like we are handling these aria attributes internally and I think it is a tad inconsistent to ask this from consumers when there are ProgressBar.Item children.

I am thinking maybe we would want to calculate the total progress internally and update the aria attributes as we do for the single progress item? Something along those lines (not a complete code)

if (children) {
      let totalProgress = 0
      React.Children.toArray(children).forEach(child => {
        if (React.isValidElement(child) && child.props.progress) {
          totalProgress += parseInt(child.props.progress)
        }
      })
    }

And use that totalProgress to calculate the value of aria attributes.

cc @siddharthkp @joshblack I would love their opinion before we decide anything concrete.

Copy link
Member

@siddharthkp siddharthkp Jul 19, 2023

Choose a reason for hiding this comment

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

Hi!

We could definitely loop through the children if the usage is predictable. However...

I couldn't find any usage of ProgressBar with multiple items, so we might be able to slide it under the rug and deprecate it safely without any remediation :)

Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react'
import React, {forwardRef} from 'react'
import styled from 'styled-components'
import {width, WidthProps} from 'styled-system'
import {get} from '../constants'
import sx, {SxProp} from '../sx'
import {warning} from '../utils/warning'

type ProgressProp = {progress?: string | number}

Expand Down Expand Up @@ -37,22 +38,37 @@ const ProgressContainer = styled.span<StyledProgressContainerProps>`
${sx};
`

export type ProgressBarProps = React.PropsWithChildren & {bg?: string} & StyledProgressContainerProps & ProgressProp

export const ProgressBar = ({
progress,
bg = 'success.emphasis',
barSize = 'default',
children,
...rest
}: ProgressBarProps) => {
if (children && progress) {
throw new Error('You should pass `progress` or children, not both.')
}

return (
<ProgressContainer barSize={barSize} {...rest}>
{children ?? <Item progress={progress} sx={{backgroundColor: bg}} />}
</ProgressContainer>
)
}
export type ProgressBarProps = React.HTMLAttributes<HTMLSpanElement> & {bg?: string} & StyledProgressContainerProps &
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@joshblack I added React.HTMLAttributes<HTMLSpanElement> here!

ProgressProp

export const ProgressBar = forwardRef<HTMLSpanElement, ProgressBarProps>(
({progress, bg = 'success.emphasis', barSize = 'default', children, ...rest}: ProgressBarProps, forwardRef) => {
if (children && progress) {
throw new Error('You should pass `progress` or children, not both.')
}

warning(
children &&
typeof (rest as React.AriaAttributes)['aria-valuenow'] === 'undefined' &&
typeof (rest as React.AriaAttributes)['aria-valuetext'] === 'undefined',
'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.',
)

const progressAsNumber = typeof progress === 'string' ? parseInt(progress, 10) : progress

const ariaAttributes = {
'aria-valuenow': progressAsNumber ? Math.round(progressAsNumber) : undefined,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
'aria-valuenow': progressAsNumber ? Math.round(progressAsNumber) : undefined,
'aria-valuenow': progressAsNumber || undefined,

Don't think we need to Math.round because parseInt already assumes base 10 and returns an integer

parseInt(20.5) === parseInt(20.5, 10) === 20

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh good point! I added the math.round before I used parseInt.

Copy link
Contributor Author

@kendallgassner kendallgassner Jul 19, 2023

Choose a reason for hiding this comment

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

Oh wait... I need it in the case that the typeof the value is a number not a string

Copy link
Member

Choose a reason for hiding this comment

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

Ah I see! My bad!

Copy link
Member

Choose a reason for hiding this comment

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

@siddharthkp one note with parseInt, I believe it will infer the base if no radix is provided based on: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt#radix

If 0 or not provided, the radix will be inferred based on string's value. Be careful — this does not always default to 10!

Copy link
Member

Choose a reason for hiding this comment

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

Oh wow, good note!

'aria-valuemin': 0,
'aria-valuemax': 100,
'aria-busy': progressAsNumber ? progressAsNumber !== 100 : false,
}

return (
<ProgressContainer ref={forwardRef} role="progressbar" barSize={barSize} {...ariaAttributes} {...rest}>
{children ?? <Item progress={progress} sx={{backgroundColor: bg}} />}
</ProgressContainer>
)
},
)

ProgressBar.displayName = 'ProgressBar'
29 changes: 22 additions & 7 deletions src/__tests__/ProgressBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,41 @@ describe('ProgressBar', () => {
})

it('should have no axe violations', async () => {
const {container} = HTMLRender(<ProgressBar progress={80} barSize="small" />)
const {container} = HTMLRender(<ProgressBar progress={80} barSize="small" aria-label="Upload test.png" />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})

it('respects the "barSize" prop', () => {
expect(render(<ProgressBar progress={80} barSize="small" />)).toHaveStyleRule('height', '5px')
expect(render(<ProgressBar progress={80} barSize="default" />)).toHaveStyleRule('height', '8px')
expect(render(<ProgressBar progress={80} barSize="large" />)).toHaveStyleRule('height', '10px')
expect(render(<ProgressBar progress={80} barSize="small" aria-label="Upload test.png" />)).toHaveStyleRule(
'height',
'5px',
)
expect(render(<ProgressBar progress={80} barSize="default" aria-label="Upload test.png" />)).toHaveStyleRule(
'height',
'8px',
)
expect(render(<ProgressBar progress={80} barSize="large" aria-label="Upload test.png" />)).toHaveStyleRule(
'height',
'10px',
)
})

it('respects the "inline" prop', () => {
expect(render(<ProgressBar progress={80} inline />)).toHaveStyleRule('display', 'inline-flex')
expect(render(<ProgressBar progress={80} inline aria-label="Upload test.png" />)).toHaveStyleRule(
'display',
'inline-flex',
)
})

it('respects the "width" prop', () => {
expect(render(<ProgressBar progress={80} inline width="100px" />)).toHaveStyleRule('width', '100px')
expect(render(<ProgressBar progress={80} inline width="100px" aria-label="Upload test.png" />)).toHaveStyleRule(
'width',
'100px',
)
})

it('respects the "progress" prop', () => {
expect(render(<ProgressBar progress={80} />)).toMatchSnapshot()
expect(render(<ProgressBar progress={80} aria-label="Upload test.png" />)).toMatchSnapshot()
})
})
10 changes: 10 additions & 0 deletions src/__tests__/__snapshots__/ProgressBar.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ exports[`ProgressBar renders consistently 1`] = `
}

<span
aria-busy={false}
aria-valuemax={100}
aria-valuemin={0}
className="c0"
role="progressbar"
>
<span
className="c1"
Expand All @@ -46,7 +50,13 @@ exports[`ProgressBar respects the "progress" prop 1`] = `
}

<span
aria-busy={true}
aria-label="Upload test.png"
aria-valuemax={100}
aria-valuemin={0}
aria-valuenow={80}
className="c0"
role="progressbar"
>
<span
className="c1"
Expand Down