Skip to content

Commit 4536b87

Browse files
joshblackcolebemis
andauthored
feat(PageLayout): add support for labeling through aria-label, aria-labelledby (#2294)
* feat(PageLayout): add support for labeling through aria-label,aria-labelledby * docs(PageLayout): add examples for aria-label,aria-labelledby * chore: add changesets * docs(PageLayout): add accessibility section * Update docs/content/PageLayout.mdx Co-authored-by: Cole Bemis <[email protected]> Co-authored-by: Cole Bemis <[email protected]>
1 parent 6445a84 commit 4536b87

File tree

8 files changed

+214
-13
lines changed

8 files changed

+214
-13
lines changed

.changeset/empty-masks-guess.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+
Add support for `aria-label` and `aria-labelledby` to `PageLayout`

.changeset/wild-books-count.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
Change the default markup of `PageLayout.Pane` from `<aside>` to `<div>`

docs/content/PageLayout.mdx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,85 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
200200
</Box>
201201
```
202202

203+
### With `aria-label`
204+
205+
Using `aria-label` along with `PageLayout.Header`, `PageLayout.Content`, or `PageLayout.Footer` creates a unique label for that landmark role that can make it easier to navigate between sections of content on a page.
206+
207+
```jsx live
208+
<PageLayout>
209+
<PageLayout.Header aria-label="header">
210+
<Placeholder label="Header" height={64} />
211+
</PageLayout.Header>
212+
<PageLayout.Content aria-label="content">
213+
<Placeholder label="Content" height={240} />
214+
</PageLayout.Content>
215+
<PageLayout.Pane>
216+
<Placeholder label="Pane" height={120} />
217+
</PageLayout.Pane>
218+
<PageLayout.Footer aria-label="footer">
219+
<Placeholder label="Footer" height={64} />
220+
</PageLayout.Footer>
221+
</PageLayout>
222+
```
223+
224+
### With `aria-labelledby`
225+
226+
Using `aria-labelledby` along with `PageLayout.Header`, `PageLayout.Content`, or `PageLayout.Footer` creates a unique label for each landmark role by using the given `id` to associate the landmark with the content with the corresponding `id`. This is helpful when you have a visible item that visually communicates the type of content which you would like to associate to the landmark itself.
227+
228+
229+
```jsx live
230+
<PageLayout>
231+
<PageLayout.Header aria-labelledby="header-label">
232+
<Placeholder id="header-label" label="Header" height={64} />
233+
</PageLayout.Header>
234+
<PageLayout.Content aria-labelledby="main-label">
235+
<Placeholder id="main-label" label="Content" height={240} />
236+
</PageLayout.Content>
237+
<PageLayout.Pane>
238+
<Placeholder label="Pane" height={120} />
239+
</PageLayout.Pane>
240+
<PageLayout.Footer aria-labelledby="footer-label">
241+
<Placeholder id="footer-label" label="Footer" height={64} />
242+
</PageLayout.Footer>
243+
</PageLayout>
244+
```
245+
246+
## Accessibility
247+
248+
The `PageLayout` component uses [landmark roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/landmark_role) for `PageLayout.Header`, `PageLayout.Content`, and `PageLayout.Footer` in order to make it easier for screen reader users to navigate between sections of the page.
249+
250+
| Component | Landmark role |
251+
| :-------- | :------------ |
252+
| `PageLayout.Header` | [`banner`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/banner_role) |
253+
| `PageLayout.Content` | [`main`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) |
254+
| `PageLayout.Footer` | [`contentinfo`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/contentinfo_role) |
255+
256+
Each component may be labeled through either `aria-label` or `aria-labelledby` in order to provide a unique label for the landmark. This can be helpful when there are multiple landmarks of the same type on the page.
257+
258+
**Links & Resources**
259+
260+
- [W3C, Landmark roles](https://w3c.github.io/aria/#landmark_roles)
261+
- [WCAG, ARIA11 Technique](https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA11)
262+
- [MDN, Landmark roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/landmark_role)
263+
264+
### Screen readers
265+
266+
Most screen readers provide a mechanism through which you can navigate between landmarks on the page. Typically, this is done through a specific keyboard shortcut or through an option in a rotor.
267+
268+
#### JAWS
269+
270+
JAWS supports landmark regions and the details in which it presents them depends on the Web Verbosity Level setting. You can navigate to the next landmark on the page by pressing `R` and the previous landmark by pressing `Shift-R`.
271+
272+
#### NVDA
273+
274+
NVDA supports landmark regions and you can navigate to the next landmark by using pressing `D` and to the previous landmark by pressing `Shift-D`. You may also list out the landmarks by pressing `Insert-F7`.
275+
276+
#### VoiceOver
277+
278+
VoiceOver supports [assigning landmark roles](https://support.apple.com/guide/voiceover/by-landmarks-vo35709/mac) to areas of a page. In order to navigate between landmarks, you can use the [rotor](https://support.apple.com/guide/voiceover/with-the-voiceover-rotor-mchlp2719/10/mac/12.0).
279+
280+
On macOS, you can open the VoiceOver rotor by pressing `VO-U`. You can navigate between lists to find the `Landmarks` list by using the Right Arrow or Left Arrow key. From that list, you can use the Down Arrow and Up Arrow keys to navigate between landmarks identified on the page.
281+
203282
## Props
204283

205284
### PageLayout
@@ -242,6 +321,16 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
242321
### PageLayout.Header
243322

244323
<PropsTable>
324+
<PropsTableRow
325+
name="aria-label"
326+
type={`string | undefined`}
327+
description="A unique label for the rendered banner landmark"
328+
/>
329+
<PropsTableRow
330+
name="aria-labelledby"
331+
type={`string | undefined`}
332+
description="An id to an element which uniquely labels the rendered banner landmark"
333+
/>
245334
<PropsTableRow
246335
name="padding"
247336
type={`| 'none'
@@ -295,6 +384,16 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
295384
### PageLayout.Content
296385

297386
<PropsTable>
387+
<PropsTableRow
388+
name="aria-label"
389+
type={`string | undefined`}
390+
description="A unique label for the rendered main landmark"
391+
/>
392+
<PropsTableRow
393+
name="aria-labelledby"
394+
type={`string | undefined`}
395+
description="An id to an element which uniquely labels the rendered main landmark"
396+
/>
298397
<PropsTableRow
299398
name="width"
300399
type={`| 'full'
@@ -428,6 +527,17 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
428527
### PageLayout.Footer
429528

430529
<PropsTable>
530+
<PropsTableRow
531+
name="aria-label"
532+
type={`string | undefined`}
533+
description="A unique label for the rendered contentinfo landmark"
534+
/>
535+
<PropsTableRow
536+
name="aria-labelledby"
537+
type={`string | undefined`}
538+
description="An id to an element which uniquely labels the rendered contentinfo landmark"
539+
/>
540+
431541
<PropsTableRow
432542
name="padding"
433543
type={`| 'none'

src/PageLayout/PageLayout.test.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import {act, render} from '@testing-library/react'
2+
import {act, render, screen} from '@testing-library/react'
33
import MatchMediaMock from 'jest-matchmedia-mock'
44
import 'react-intersection-observer/test-utils'
55
import {ThemeProvider} from '..'
@@ -116,4 +116,44 @@ describe('PageLayout', () => {
116116

117117
expect(getByText('Pane')).toBeVisible()
118118
})
119+
120+
it('should support labeling landmarks through `aria-label`', () => {
121+
render(
122+
<ThemeProvider>
123+
<PageLayout>
124+
<PageLayout.Header aria-label="header">Header</PageLayout.Header>
125+
<PageLayout.Content aria-label="content">Content</PageLayout.Content>
126+
<PageLayout.Pane>Pane</PageLayout.Pane>
127+
<PageLayout.Footer aria-label="footer">Footer</PageLayout.Footer>
128+
</PageLayout>
129+
</ThemeProvider>
130+
)
131+
132+
expect(screen.getByRole('banner')).toHaveAccessibleName('header')
133+
expect(screen.getByRole('main')).toHaveAccessibleName('content')
134+
expect(screen.getByRole('contentinfo')).toHaveAccessibleName('footer')
135+
})
136+
137+
it('should support labeling landmarks through `aria-labelledby`', () => {
138+
render(
139+
<ThemeProvider>
140+
<PageLayout>
141+
<PageLayout.Header aria-labelledby="header-label">
142+
<span id="header-label">header</span>
143+
</PageLayout.Header>
144+
<PageLayout.Content aria-labelledby="content-label">
145+
<span id="content-label">content</span>
146+
</PageLayout.Content>
147+
<PageLayout.Pane>Pane</PageLayout.Pane>
148+
<PageLayout.Footer aria-labelledby="footer-label">
149+
<span id="footer-label">footer</span>
150+
</PageLayout.Footer>
151+
</PageLayout>
152+
</ThemeProvider>
153+
)
154+
155+
expect(screen.getByRole('banner')).toHaveAccessibleName('header')
156+
expect(screen.getByRole('main')).toHaveAccessibleName('content')
157+
expect(screen.getByRole('contentinfo')).toHaveAccessibleName('footer')
158+
})
119159
})

src/PageLayout/PageLayout.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps>> = ({varia
196196
// PageLayout.Header
197197

198198
export type PageLayoutHeaderProps = {
199+
/**
200+
* A unique label for the rendered banner landmark
201+
*/
202+
'aria-label'?: React.AriaAttributes['aria-label']
203+
204+
/**
205+
* An id to an element which uniquely labels the rendered banner landmark
206+
*/
207+
'aria-labelledby'?: React.AriaAttributes['aria-labelledby']
208+
199209
padding?: keyof typeof SPACING_MAP
200210
divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'>
201211
/**
@@ -217,6 +227,8 @@ export type PageLayoutHeaderProps = {
217227
} & SxProp
218228

219229
const Header: React.FC<React.PropsWithChildren<PageLayoutHeaderProps>> = ({
230+
'aria-label': label,
231+
'aria-labelledby': labelledBy,
220232
padding = 'none',
221233
divider = 'none',
222234
dividerWhenNarrow = 'inherit',
@@ -236,6 +248,8 @@ const Header: React.FC<React.PropsWithChildren<PageLayoutHeaderProps>> = ({
236248
return (
237249
<Box
238250
as="header"
251+
aria-label={label}
252+
aria-labelledby={labelledBy}
239253
hidden={isHidden}
240254
sx={merge<BetterSystemStyleObject>(
241255
{
@@ -258,6 +272,15 @@ Header.displayName = 'PageLayout.Header'
258272
// PageLayout.Content
259273

260274
export type PageLayoutContentProps = {
275+
/**
276+
* A unique label for the rendered main landmark
277+
*/
278+
'aria-label'?: React.AriaAttributes['aria-label']
279+
280+
/**
281+
* An id to an element which uniquely labels the rendered main landmark
282+
*/
283+
'aria-labelledby'?: React.AriaAttributes['aria-labelledby']
261284
width?: keyof typeof contentWidths
262285
padding?: keyof typeof SPACING_MAP
263286
hidden?: boolean | ResponsiveValue<boolean>
@@ -272,6 +295,8 @@ const contentWidths = {
272295
}
273296

274297
const Content: React.FC<React.PropsWithChildren<PageLayoutContentProps>> = ({
298+
'aria-label': label,
299+
'aria-labelledby': labelledBy,
275300
width = 'full',
276301
padding = 'none',
277302
hidden = false,
@@ -283,6 +308,8 @@ const Content: React.FC<React.PropsWithChildren<PageLayoutContentProps>> = ({
283308
return (
284309
<Box
285310
as="main"
311+
aria-label={label}
312+
aria-labelledby={labelledBy}
286313
sx={merge<BetterSystemStyleObject>(
287314
{
288315
display: isHidden ? 'none' : 'flex',
@@ -419,7 +446,6 @@ const Pane: React.FC<React.PropsWithChildren<PageLayoutPaneProps>> = ({
419446

420447
return (
421448
<Box
422-
as="aside"
423449
// eslint-disable-next-line @typescript-eslint/no-explicit-any
424450
sx={(theme: any) =>
425451
merge<BetterSystemStyleObject>(
@@ -477,6 +503,15 @@ Pane.displayName = 'PageLayout.Pane'
477503
// PageLayout.Footer
478504

479505
export type PageLayoutFooterProps = {
506+
/**
507+
* A unique label for the rendered contentinfo landmark
508+
*/
509+
'aria-label'?: React.AriaAttributes['aria-label']
510+
511+
/**
512+
* An id to an element which uniquely labels the rendered contentinfo landmark
513+
*/
514+
'aria-labelledby'?: React.AriaAttributes['aria-labelledby']
480515
padding?: keyof typeof SPACING_MAP
481516
divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'>
482517
/**
@@ -498,6 +533,8 @@ export type PageLayoutFooterProps = {
498533
} & SxProp
499534

500535
const Footer: React.FC<React.PropsWithChildren<PageLayoutFooterProps>> = ({
536+
'aria-label': label,
537+
'aria-labelledby': labelledBy,
501538
padding = 'none',
502539
divider = 'none',
503540
dividerWhenNarrow = 'inherit',
@@ -517,6 +554,8 @@ const Footer: React.FC<React.PropsWithChildren<PageLayoutFooterProps>> = ({
517554
return (
518555
<Box
519556
as="footer"
557+
aria-label={label}
558+
aria-labelledby={labelledBy}
520559
hidden={isHidden}
521560
sx={merge<BetterSystemStyleObject>(
522561
{

src/PageLayout/__snapshots__/PageLayout.test.tsx.snap

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ exports[`PageLayout renders condensed layout 1`] = `
190190
class=""
191191
/>
192192
</main>
193-
<aside
193+
<div
194194
class="c7"
195195
>
196196
<div
@@ -204,7 +204,7 @@ exports[`PageLayout renders condensed layout 1`] = `
204204
>
205205
Pane
206206
</div>
207-
</aside>
207+
</div>
208208
<footer
209209
class="c11"
210210
>
@@ -458,7 +458,7 @@ exports[`PageLayout renders default layout 1`] = `
458458
class=""
459459
/>
460460
</main>
461-
<aside
461+
<div
462462
class="c7"
463463
>
464464
<div
@@ -472,7 +472,7 @@ exports[`PageLayout renders default layout 1`] = `
472472
>
473473
Pane
474474
</div>
475-
</aside>
475+
</div>
476476
<footer
477477
class="c11"
478478
>
@@ -726,7 +726,7 @@ exports[`PageLayout renders pane in different position when narrow 1`] = `
726726
class=""
727727
/>
728728
</main>
729-
<aside
729+
<div
730730
class="c7"
731731
>
732732
<div
@@ -740,7 +740,7 @@ exports[`PageLayout renders pane in different position when narrow 1`] = `
740740
>
741741
Pane
742742
</div>
743-
</aside>
743+
</div>
744744
<footer
745745
class="c11"
746746
>
@@ -994,7 +994,7 @@ exports[`PageLayout renders with dividers 1`] = `
994994
class=""
995995
/>
996996
</main>
997-
<aside
997+
<div
998998
class="c7"
999999
>
10001000
<div
@@ -1008,7 +1008,7 @@ exports[`PageLayout renders with dividers 1`] = `
10081008
>
10091009
Pane
10101010
</div>
1011-
</aside>
1011+
</div>
10121012
<footer
10131013
class="c10"
10141014
>

0 commit comments

Comments
 (0)