Skip to content

Commit e8db0f2

Browse files
committed
fix(ui-breadcrumb): add and update aria tags in Breadcrumb and in documentation
Closes: INSTUI-4268
1 parent 7273c3a commit e8db0f2

File tree

5 files changed

+129
-9
lines changed

5 files changed

+129
-9
lines changed

packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,15 @@ class BreadcrumbLink extends Component<BreadcrumbLinkProps> {
5454
}
5555

5656
render() {
57-
const { children, href, renderIcon, iconPlacement, onClick, onMouseEnter } =
58-
this.props
57+
const {
58+
children,
59+
href,
60+
renderIcon,
61+
iconPlacement,
62+
onClick,
63+
onMouseEnter,
64+
isCurrentPage
65+
} = this.props
5966

6067
const props = omitProps(this.props, BreadcrumbLink.allowedProps)
6168

@@ -69,6 +76,7 @@ class BreadcrumbLink extends Component<BreadcrumbLinkProps> {
6976
onMouseEnter={onMouseEnter}
7077
isWithinText={false}
7178
elementRef={this.handleRef}
79+
{...(isCurrentPage && { 'aria-current': 'page' })}
7280
>
7381
<TruncateText>{children}</TruncateText>
7482
</Link>

packages/ui-breadcrumb/src/Breadcrumb/BreadcrumbLink/props.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ type BreadcrumbLinkOwnProps = {
6363
* Place the icon before or after the text in the Breadcrumb.Link
6464
*/
6565
iconPlacement?: 'start' | 'end'
66+
/**
67+
* Whether the page this breadcrumb points to is the current one. If true, it sets aria-current="page".
68+
* If this prop is not set to true on any breadcrumb element, the one recieving the aria-current="page" will always be the last element, unless the last element's isCurrentPage prop is explicity set to false.
69+
*/
70+
isCurrentPage?: boolean
6671
}
6772

6873
type PropKeys = keyof BreadcrumbLinkOwnProps
@@ -89,7 +94,8 @@ const propTypes: PropValidators<PropKeys> = {
8994
onMouseEnter: PropTypes.func,
9095
size: PropTypes.oneOf(['small', 'medium', 'large']),
9196
renderIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
92-
iconPlacement: PropTypes.oneOf(['start', 'end'])
97+
iconPlacement: PropTypes.oneOf(['start', 'end']),
98+
isCurrentPage: PropTypes.bool
9399
}
94100

95101
const allowedProps: AllowedPropKeys = [
@@ -99,7 +105,8 @@ const allowedProps: AllowedPropKeys = [
99105
'onClick',
100106
'onMouseEnter',
101107
'renderIcon',
102-
'size'
108+
'size',
109+
'isCurrentPage'
103110
]
104111

105112
export type { BreadcrumbLinkProps }

packages/ui-breadcrumb/src/Breadcrumb/README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type: example
2323
{(props, matches) => {
2424
if (matches.includes('tablet')) {
2525
return (
26-
<Breadcrumb label="You are here:">
26+
<Breadcrumb label="breadcrumb">
2727
<Breadcrumb.Link href="#">Student Forecast</Breadcrumb.Link>
2828
<Breadcrumb.Link href="#">University of Utah</Breadcrumb.Link>
2929
<Breadcrumb.Link href="#">University of Utah Colleges</Breadcrumb.Link>
@@ -52,7 +52,7 @@ Change the `size` prop to control the font-size of the breadcrumbs (default is `
5252
type: example
5353
---
5454
<div>
55-
<Breadcrumb size="small" label="You are here:" margin="none none medium">
55+
<Breadcrumb size="small" label="breadcrumb" margin="none none medium">
5656
<Breadcrumb.Link href="https://instructure.github.io/instructure-ui/">English 204</Breadcrumb.Link>
5757
<Breadcrumb.Link
5858
onClick={function () {
@@ -65,7 +65,7 @@ type: example
6565
<Breadcrumb.Link>Rabbit Is Rich</Breadcrumb.Link>
6666
</Breadcrumb>
6767
<View as="div" width="40rem">
68-
<Breadcrumb label="You are here:" margin="none none medium">
68+
<Breadcrumb label="breadcrumb" margin="none none medium">
6969
<Breadcrumb.Link href="https://instructure.github.io/instructure-ui/">English 204</Breadcrumb.Link>
7070
<Breadcrumb.Link
7171
onClick={function () {
@@ -78,7 +78,7 @@ type: example
7878
<Breadcrumb.Link>Rabbit Is Rich</Breadcrumb.Link>
7979
</Breadcrumb>
8080
</View>
81-
<Breadcrumb size="large" label="You are here:">
81+
<Breadcrumb size="large" label="breadcrumb">
8282
<Breadcrumb.Link href="https://instructure.github.io/instructure-ui/">English 204</Breadcrumb.Link>
8383
<Breadcrumb.Link
8484
onClick={function () {
@@ -126,3 +126,16 @@ type: embed
126126
</Figure>
127127
</Guidelines>
128128
```
129+
130+
```js
131+
---
132+
type: embed
133+
---
134+
<Guidelines>
135+
<Figure recommendation="a11y" title="Accessibility">
136+
<Figure.Item>
137+
To indicate the current element within a breadcrumb, the <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current">aria-current</a> attribute is used. In this component, aria-current="page" will automatically be applied to the last element, and we recommend that the current page always be the last element in the breadcrumb. If the last element is not the current page, the isCurrentPage property should be applied to the relevant Breadcrumb.Link to ensure compatibility with screen readers.
138+
</Figure.Item>
139+
</Figure>
140+
</Guidelines>
141+
```

packages/ui-breadcrumb/src/Breadcrumb/__new-tests__/Breadcrumb.test.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,68 @@ describe('<Breadcrumb />', () => {
111111
expect(icon).toBeInTheDocument()
112112
expect(icon).toHaveAttribute('aria-hidden', 'true')
113113
})
114+
115+
it('should add aria-current="page" to the last element by default', () => {
116+
const { container } = render(
117+
<Breadcrumb label={TEST_LABEL}>
118+
<Breadcrumb.Link href={TEST_LINK}>{TEST_TEXT_01}</Breadcrumb.Link>
119+
<Breadcrumb.Link>{TEST_TEXT_02}</Breadcrumb.Link>
120+
</Breadcrumb>
121+
)
122+
const links = container.querySelectorAll('[class$="--block-link"]')
123+
const firstLink = links[0]
124+
const lastLink = links[links.length - 1]
125+
126+
expect(firstLink).not.toHaveAttribute('aria-current', 'page')
127+
expect(lastLink).toHaveAttribute('aria-current', 'page')
128+
})
129+
130+
it('should add aria-current="page" to the element if isCurrent is true', () => {
131+
const { container } = render(
132+
<Breadcrumb label={TEST_LABEL}>
133+
<Breadcrumb.Link isCurrentPage href={TEST_LINK}>
134+
{TEST_TEXT_01}
135+
</Breadcrumb.Link>
136+
<Breadcrumb.Link>{TEST_TEXT_02}</Breadcrumb.Link>
137+
</Breadcrumb>
138+
)
139+
const links = container.querySelectorAll('[class$="--block-link"]')
140+
const firstLink = links[0]
141+
const lastLink = links[links.length - 1]
142+
143+
expect(firstLink).toHaveAttribute('aria-current', 'page')
144+
expect(lastLink).not.toHaveAttribute('aria-current', 'page')
145+
})
146+
147+
it('should throw a warning when multiple elements have isCurrent set to true ', () => {
148+
render(
149+
<Breadcrumb label={TEST_LABEL}>
150+
<Breadcrumb.Link isCurrentPage href={TEST_LINK}>
151+
{TEST_TEXT_01}
152+
</Breadcrumb.Link>
153+
<Breadcrumb.Link isCurrentPage>{TEST_TEXT_02}</Breadcrumb.Link>
154+
</Breadcrumb>
155+
)
156+
157+
expect(consoleWarningMock).toHaveBeenCalledWith(
158+
expect.stringContaining(
159+
'Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.'
160+
)
161+
)
162+
})
163+
164+
it('should not add aria-current="page" to the last element if it set to false', () => {
165+
const { container } = render(
166+
<Breadcrumb label={TEST_LABEL}>
167+
<Breadcrumb.Link href={TEST_LINK}>{TEST_TEXT_01}</Breadcrumb.Link>
168+
<Breadcrumb.Link isCurrentPage={false}>{TEST_TEXT_02}</Breadcrumb.Link>
169+
</Breadcrumb>
170+
)
171+
const links = container.querySelectorAll('[class$="--block-link"]')
172+
const firstLink = links[0]
173+
const lastLink = links[links.length - 1]
174+
175+
expect(firstLink).not.toHaveAttribute('aria-current', 'page')
176+
expect(lastLink).not.toHaveAttribute('aria-current', 'page')
177+
})
114178
})

packages/ui-breadcrumb/src/Breadcrumb/index.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ class Breadcrumb extends Component<BreadcrumbProps> {
6262
this.ref = el
6363
}
6464

65+
addAriaCurrent = (child: React.ReactNode) => {
66+
const updatedChild = React.cloneElement(
67+
child as React.ReactElement<{ 'aria-current'?: string }>,
68+
{
69+
'aria-current': 'page'
70+
}
71+
)
72+
return updatedChild
73+
}
74+
6575
componentDidMount() {
6676
this.props.makeStyles?.()
6777
}
@@ -77,10 +87,28 @@ class Breadcrumb extends Component<BreadcrumbProps> {
7787
const inlineStyle = {
7888
maxWidth: `${Math.floor(100 / numChildren)}%`
7989
}
90+
let isAriaCurrentSet = false
91+
8092
return React.Children.map(children, (child, index) => {
93+
const isLastElement = index === numChildren - 1
94+
if (React.isValidElement(child)) {
95+
const isCurrentPage = child.props.isCurrentPage || false
96+
if (isAriaCurrentSet && isCurrentPage) {
97+
console.warn(
98+
`Warning: Multiple elements with isCurrentPage=true found. Only one element should be set to current.`
99+
)
100+
}
101+
if (isCurrentPage) {
102+
isAriaCurrentSet = true
103+
}
104+
}
81105
return (
82106
<li css={styles?.crumb} style={inlineStyle}>
83-
{child}
107+
{!isAriaCurrentSet &&
108+
isLastElement &&
109+
(child as React.ReactElement).props.isCurrentPage !== false
110+
? this.addAriaCurrent(child)
111+
: child}
84112
{index < numChildren - 1 && (
85113
<IconArrowOpenEndSolid color="auto" css={styles?.separator} />
86114
)}

0 commit comments

Comments
 (0)