Skip to content
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

Utilize aria-describedby on all ActionList descriptions #4666

Merged
merged 14 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions .changeset/lovely-days-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Adds `aria-describedby` to `ActionList` inline descriptions, adds `aria-labelledby` to `ActionList.TrailingVisual`
TylerJDev marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 36 additions & 0 deletions packages/react/src/ActionList/ActionList.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Box from '../Box'
import Label from '../Label'
import Heading from '../Heading'
import {AnchoredOverlay} from '../AnchoredOverlay'
import CounterLabel from '../CounterLabel'
import {
EyeIcon,
BookIcon,
Expand All @@ -25,6 +26,9 @@ import {
PeopleIcon,
FileDirectoryIcon,
PlusCircleIcon,
GitPullRequestIcon,
IssueOpenedIcon,
ProjectIcon,
LinkExternalIcon,
} from '@primer/octicons-react'
import {FeatureFlags} from '../FeatureFlags'
Expand Down Expand Up @@ -726,6 +730,38 @@ export const GroupWithFilledTitle = () => {
)
}

export const WithCustomTrailingVisuals = () => (
<ActionList>
<ActionList.Item>
<ActionList.LeadingVisual>
<IssueOpenedIcon />
</ActionList.LeadingVisual>
Issues
<ActionList.TrailingVisual>
<CounterLabel>20</CounterLabel>
</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item>
<ActionList.LeadingVisual>
<GitPullRequestIcon />
</ActionList.LeadingVisual>
PRs
<ActionList.TrailingVisual>
<CounterLabel>12</CounterLabel>
</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item>
<ActionList.LeadingVisual>
<ProjectIcon />
</ActionList.LeadingVisual>
Projects
<ActionList.TrailingVisual>
<CounterLabel>2</CounterLabel>
</ActionList.TrailingVisual>
</ActionList.Item>
</ActionList>
)

export const ActionListWithButtonSemantics = () => {
return (
<FeatureFlags flags={{primer_react_action_list_item_as_button: true}}>
Expand Down
20 changes: 20 additions & 0 deletions packages/react/src/ActionList/ActionList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,26 @@ describe('ActionList', () => {
expect(list).toHaveAttribute('aria-label', heading.textContent)
})

it('descriptions should be referenced via aria-describedby', async () => {
const {getByRole} = HTMLRender(
<ActionList>
<ActionList.Item>
Item <ActionList.Description>Inline description</ActionList.Description>
</ActionList.Item>
<ActionList.Item>
Item
<ActionList.Description variant="block">Block description</ActionList.Description>
</ActionList.Item>
</ActionList>,
)

const inlineItem = getByRole('listitem', {description: 'Inline description'})
const blockItem = getByRole('listitem', {description: 'Block description'})

expect(inlineItem).toBeInTheDocument()
expect(blockItem).toBeInTheDocument()
})

it('should render ActionList.Item as button when feature flag is enabled', async () => {
const featureFlag = {
primer_react_action_list_item_as_button: true,
Expand Down
19 changes: 14 additions & 5 deletions packages/react/src/ActionList/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
const labelId = `${itemId}--label`
const inlineDescriptionId = `${itemId}--inline-description`
const blockDescriptionId = `${itemId}--block-description`
const trailingVisualId = `${itemId}--trailing-visual`
const inactiveWarningId = inactive && !showInactiveIndicator ? `${itemId}--warning-message` : undefined

const ButtonItemWrapper = React.forwardRef(({as: Component = 'button', children, ...props}, forwardedRef) => {
Expand Down Expand Up @@ -294,10 +295,11 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
'data-inactive': inactive ? true : undefined,
'data-loading': loading && !inactive ? true : undefined,
tabIndex: disabled || showInactiveIndicator ? undefined : 0,
'aria-labelledby': `${labelId} ${slots.inlineDescription ? inlineDescriptionId : ''}`,
'aria-describedby': slots.blockDescription
? [blockDescriptionId, inactiveWarningId].join(' ')
: inactiveWarningId,
'aria-labelledby': `${labelId} ${slots.trailingVisual ? trailingVisualId : ''}`,
Copy link
Member

@siddharthkp siddharthkp Jul 30, 2024

Choose a reason for hiding this comment

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

  1. How do we decide if trailing visual should be in label or description?
  2. Are there places in dotcom which would end up with the wrong label / description from this?

Re: moving inline description from label to description, can we create a different PR for that because I'm less confident about that change. (I brought inline description from aria-describedby to aria-label #1599 after an accessibility review 😅)

I see from https://github.com/github/primer/issues/2501#issuecomment-1674987399,

We should probably have all instances of this component be referenced to via aria-describedby, instead of only the block variant. This is because the content in description might be a bit verbose to be included in the accessible name, regardless of the variant used.

This is really old, so forgive me for missing the details, but I think inline description was assumed to be short and label-like, which block description is for longer and even multi-line text. It's not mentioned in the design docs, so I wonder if primer-designers have thoughts on this as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How do we decide if trailing visual should be in label or description?

I've added it as a label via aria-labelledby instead of a description because if there's text content within trailing visual, it's usually short and connects directly to the main item's text (accessibl, e.g.

<ActionList>
  <ActionList.Item>Tokens Available 
    <ActionList.TrailingVisual>8</ActionList.TrailingVisual>
  </ActionList.Item>
</ActionList>

Accessible name would be: "Tokens Available 8".

I mainly based this on examples I've seen in the wild. If we added it as a description, there's a chance it might be ignored, so I think aria-labelledby is a fair bet.

Are there places in dotcom which would end up with the wrong label / description from this?

I'd say it's very unlikely, since any text within the trailing visual isn't attached to ActionList.Item's accessible name/description currently.

Re: moving inline description from label to description, can we create a different PR for that because I'm less confident about that change.

Yup that's fair! I think it's worth its own discussion to see if we'd want to switch it or not, as it definitely works both ways, as part of the accessible name, or as part of the description. I'll take that change out of this PR and create another so that we can discuss/see if we want to include it or not!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should now only include the proposed changed for TrailingVisual!

Copy link
Member

@siddharthkp siddharthkp Jul 31, 2024

Choose a reason for hiding this comment

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

I mainly based this on examples I've seen in the wild.

Does this also include within menus/selectpanels?

(Happy to go ahead with this, either of label vs description is better than aria-hidden i guess)

'aria-describedby':
TylerJDev marked this conversation as resolved.
Show resolved Hide resolved
`${slots.blockDescription ? blockDescriptionId : ''} ${inactiveWarningId ?? ''} ${
slots.inlineDescription ? inlineDescriptionId : ''
}`.trim() || undefined,
...(includeSelectionAttribute && {[itemSelectionAttribute]: selected}),
role: itemRole,
id: itemId,
Expand Down Expand Up @@ -327,7 +329,14 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(

return (
<ItemContext.Provider
value={{variant, disabled, inactive: Boolean(inactiveText), inlineDescriptionId, blockDescriptionId}}
value={{
variant,
disabled,
inactive: Boolean(inactiveText),
inlineDescriptionId,
blockDescriptionId,
trailingVisualId,
}}
>
<LiBox
ref={!buttonSemanticsFeatureFlag || listSemantics ? forwardedRef : null}
Expand Down
3 changes: 2 additions & 1 deletion packages/react/src/ActionList/Visuals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ export const LeadingVisual: React.FC<React.PropsWithChildren<VisualProps>> = ({s

export type ActionListTrailingVisualProps = VisualProps
export const TrailingVisual: React.FC<React.PropsWithChildren<VisualProps>> = ({sx = {}, ...props}) => {
const {variant, disabled, inactive} = React.useContext(ItemContext)
const {variant, disabled, inactive, trailingVisualId} = React.useContext(ItemContext)
return (
<Box
id={trailingVisualId}
as="span"
sx={merge(
{
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/ActionList/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type MenuItemProps = {
export type ItemContext = Pick<ActionListItemProps, 'variant' | 'disabled'> & {
inlineDescriptionId?: string
blockDescriptionId?: string
trailingVisualId?: string
inactive?: boolean
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1274,7 +1274,7 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
<button
aria-controls=":r27:"
aria-expanded="true"
aria-labelledby=":r26:--label "
aria-labelledby=":r26:--label :r26:--trailing-visual"
class="c3 c4"
id=":r26:"
tabindex="0"
Expand All @@ -1294,6 +1294,7 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
</span>
<span
class="c1 c8"
id=":r26:--trailing-visual"
>
<svg
aria-hidden="true"
Expand Down Expand Up @@ -1773,7 +1774,7 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
<button
aria-controls=":r21:"
aria-expanded="false"
aria-labelledby=":r20:--label "
aria-labelledby=":r20:--label :r20:--trailing-visual"
class="c3 c4"
id=":r20:"
tabindex="0"
Expand All @@ -1793,6 +1794,7 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
</span>
<span
class="c1 c8"
id=":r20:--trailing-visual"
>
<svg
aria-hidden="true"
Expand Down
Loading