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

feat(PageLayout): add support for labeling through aria-label, aria-labelledby #2294

Merged
merged 7 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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/empty-masks-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Add support for `aria-label` and `aria-labelledby` to `PageLayout`
5 changes: 5 additions & 0 deletions .changeset/wild-books-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': patch
---

Change the default markup of `PageLayout.Pane` from `<aside>` to `<div>`
110 changes: 110 additions & 0 deletions docs/content/PageLayout.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,85 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
</Box>
```

### With `aria-label`

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.

```jsx live
<PageLayout>
<PageLayout.Header aria-label="header">
<Placeholder label="Header" height={64} />
</PageLayout.Header>
<PageLayout.Content aria-label="content">
<Placeholder label="Content" height={240} />
</PageLayout.Content>
<PageLayout.Pane>
<Placeholder label="Pane" height={120} />
</PageLayout.Pane>
<PageLayout.Footer aria-label="footer">
<Placeholder label="Footer" height={64} />
</PageLayout.Footer>
</PageLayout>
```

### With `aria-labelledby`

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.


```jsx live
<PageLayout>
<PageLayout.Header aria-labelledby="header-label">
<Placeholder id="header-label" label="Header" height={64} />
</PageLayout.Header>
<PageLayout.Content aria-labelledby="main-label">
<Placeholder id="main-label" label="Content" height={240} />
</PageLayout.Content>
<PageLayout.Pane>
<Placeholder label="Pane" height={120} />
</PageLayout.Pane>
<PageLayout.Footer aria-labelledby="footer-label">
<Placeholder id="footer-label" label="Footer" height={64} />
</PageLayout.Footer>
</PageLayout>
```

## Accessibility

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.

| Component | Landmark role |
| :-------- | :------------ |
| `PageLayout.Header` | [`banner`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/banner_role) |
| `PageLayout.Content` | [`main`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/main_role) |
| `PageLayout.Footer` | [`contentinfo`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/contentinfo_role) |

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.

**Links & Resources**

- [W3C, Landmark roles](https://w3c.github.io/aria/#landmark_roles)
- [WCAG, ARIA11 Technique](https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA11)
- [MDN, Landmark roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/landmark_role)

### Screen readers

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.

#### JAWS

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`.

#### NVDA

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`.

#### VoiceOver

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).

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.

## Props

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

<PropsTable>
<PropsTableRow
name="aria-label"
type={`string | undefined`}
description="A unique label for the rendered banner landmark"
/>
<PropsTableRow
name="aria-labelledby"
type={`string | undefined`}
description="An id to an element which uniquely labels the rendered banner landmark"
/>
<PropsTableRow
name="padding"
type={`| 'none'
Expand Down Expand Up @@ -295,6 +384,16 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
### PageLayout.Content

<PropsTable>
<PropsTableRow
name="aria-label"
type={`string | undefined`}
description="A unique label for the rendered main landmark"
/>
<PropsTableRow
name="aria-labelledby"
type={`string | undefined`}
description="An id to an element which uniquely labels the rendered main landmark"
/>
<PropsTableRow
name="width"
type={`| 'full'
Expand Down Expand Up @@ -428,6 +527,17 @@ See [storybook](https://primer.style/react/storybook?path=/story/layout-pagelayo
### PageLayout.Footer

<PropsTable>
<PropsTableRow
name="aria-label"
type={`string | undefined`}
description="A unique label for the rendered contentinfo landmark"
/>
<PropsTableRow
name="aria-labelledby"
type={`string | undefined`}
description="An id to an element which uniquely labels the rendered contentinfo landmark"
/>

<PropsTableRow
name="padding"
type={`| 'none'
Expand Down
42 changes: 41 additions & 1 deletion src/PageLayout/PageLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import {act, render} from '@testing-library/react'
import {act, render, screen} from '@testing-library/react'
import MatchMediaMock from 'jest-matchmedia-mock'
import 'react-intersection-observer/test-utils'
import {ThemeProvider} from '..'
Expand Down Expand Up @@ -116,4 +116,44 @@ describe('PageLayout', () => {

expect(getByText('Pane')).toBeVisible()
})

it('should support labeling landmarks through `aria-label`', () => {
render(
<ThemeProvider>
<PageLayout>
<PageLayout.Header aria-label="header">Header</PageLayout.Header>
<PageLayout.Content aria-label="content">Content</PageLayout.Content>
<PageLayout.Pane>Pane</PageLayout.Pane>
<PageLayout.Footer aria-label="footer">Footer</PageLayout.Footer>
</PageLayout>
</ThemeProvider>
)

expect(screen.getByRole('banner')).toHaveAccessibleName('header')
expect(screen.getByRole('main')).toHaveAccessibleName('content')
expect(screen.getByRole('contentinfo')).toHaveAccessibleName('footer')
})

it('should support labeling landmarks through `aria-labelledby`', () => {
render(
<ThemeProvider>
<PageLayout>
<PageLayout.Header aria-labelledby="header-label">
<span id="header-label">header</span>
</PageLayout.Header>
<PageLayout.Content aria-labelledby="content-label">
<span id="content-label">content</span>
</PageLayout.Content>
<PageLayout.Pane>Pane</PageLayout.Pane>
<PageLayout.Footer aria-labelledby="footer-label">
<span id="footer-label">footer</span>
</PageLayout.Footer>
</PageLayout>
</ThemeProvider>
)

expect(screen.getByRole('banner')).toHaveAccessibleName('header')
expect(screen.getByRole('main')).toHaveAccessibleName('content')
expect(screen.getByRole('contentinfo')).toHaveAccessibleName('footer')
})
})
41 changes: 40 additions & 1 deletion src/PageLayout/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ const VerticalDivider: React.FC<React.PropsWithChildren<DividerProps>> = ({varia
// PageLayout.Header

export type PageLayoutHeaderProps = {
/**
* A unique label for the rendered banner landmark
*/
'aria-label'?: React.AriaAttributes['aria-label']

/**
* An id to an element which uniquely labels the rendered banner landmark
*/
'aria-labelledby'?: React.AriaAttributes['aria-labelledby']

padding?: keyof typeof SPACING_MAP
divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'>
/**
Expand All @@ -217,6 +227,8 @@ export type PageLayoutHeaderProps = {
} & SxProp

const Header: React.FC<React.PropsWithChildren<PageLayoutHeaderProps>> = ({
'aria-label': label,
'aria-labelledby': labelledBy,
padding = 'none',
divider = 'none',
dividerWhenNarrow = 'inherit',
Expand All @@ -236,6 +248,8 @@ const Header: React.FC<React.PropsWithChildren<PageLayoutHeaderProps>> = ({
return (
<Box
as="header"
aria-label={label}
aria-labelledby={labelledBy}
hidden={isHidden}
sx={merge<BetterSystemStyleObject>(
{
Expand All @@ -258,6 +272,15 @@ Header.displayName = 'PageLayout.Header'
// PageLayout.Content

export type PageLayoutContentProps = {
/**
* A unique label for the rendered main landmark
*/
'aria-label'?: React.AriaAttributes['aria-label']

/**
* An id to an element which uniquely labels the rendered main landmark
*/
'aria-labelledby'?: React.AriaAttributes['aria-labelledby']
width?: keyof typeof contentWidths
padding?: keyof typeof SPACING_MAP
hidden?: boolean | ResponsiveValue<boolean>
Expand All @@ -272,6 +295,8 @@ const contentWidths = {
}

const Content: React.FC<React.PropsWithChildren<PageLayoutContentProps>> = ({
'aria-label': label,
'aria-labelledby': labelledBy,
width = 'full',
padding = 'none',
hidden = false,
Expand All @@ -283,6 +308,8 @@ const Content: React.FC<React.PropsWithChildren<PageLayoutContentProps>> = ({
return (
<Box
as="main"
aria-label={label}
aria-labelledby={labelledBy}
sx={merge<BetterSystemStyleObject>(
{
display: isHidden ? 'none' : 'flex',
Expand Down Expand Up @@ -419,7 +446,6 @@ const Pane: React.FC<React.PropsWithChildren<PageLayoutPaneProps>> = ({

return (
<Box
as="aside"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sx={(theme: any) =>
merge<BetterSystemStyleObject>(
Expand Down Expand Up @@ -477,6 +503,15 @@ Pane.displayName = 'PageLayout.Pane'
// PageLayout.Footer

export type PageLayoutFooterProps = {
/**
* A unique label for the rendered contentinfo landmark
*/
'aria-label'?: React.AriaAttributes['aria-label']

/**
* An id to an element which uniquely labels the rendered contentinfo landmark
*/
'aria-labelledby'?: React.AriaAttributes['aria-labelledby']
padding?: keyof typeof SPACING_MAP
divider?: 'none' | 'line' | ResponsiveValue<'none' | 'line', 'none' | 'line' | 'filled'>
/**
Expand All @@ -498,6 +533,8 @@ export type PageLayoutFooterProps = {
} & SxProp

const Footer: React.FC<React.PropsWithChildren<PageLayoutFooterProps>> = ({
'aria-label': label,
'aria-labelledby': labelledBy,
padding = 'none',
divider = 'none',
dividerWhenNarrow = 'inherit',
Expand All @@ -517,6 +554,8 @@ const Footer: React.FC<React.PropsWithChildren<PageLayoutFooterProps>> = ({
return (
<Box
as="footer"
aria-label={label}
aria-labelledby={labelledBy}
hidden={isHidden}
sx={merge<BetterSystemStyleObject>(
{
Expand Down
16 changes: 8 additions & 8 deletions src/PageLayout/__snapshots__/PageLayout.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ exports[`PageLayout renders condensed layout 1`] = `
class=""
/>
</main>
<aside
<div
class="c7"
>
<div
Expand All @@ -204,7 +204,7 @@ exports[`PageLayout renders condensed layout 1`] = `
>
Pane
</div>
</aside>
</div>
<footer
class="c11"
>
Expand Down Expand Up @@ -458,7 +458,7 @@ exports[`PageLayout renders default layout 1`] = `
class=""
/>
</main>
<aside
<div
class="c7"
>
<div
Expand All @@ -472,7 +472,7 @@ exports[`PageLayout renders default layout 1`] = `
>
Pane
</div>
</aside>
</div>
<footer
class="c11"
>
Expand Down Expand Up @@ -726,7 +726,7 @@ exports[`PageLayout renders pane in different position when narrow 1`] = `
class=""
/>
</main>
<aside
<div
class="c7"
>
<div
Expand All @@ -740,7 +740,7 @@ exports[`PageLayout renders pane in different position when narrow 1`] = `
>
Pane
</div>
</aside>
</div>
<footer
class="c11"
>
Expand Down Expand Up @@ -994,7 +994,7 @@ exports[`PageLayout renders with dividers 1`] = `
class=""
/>
</main>
<aside
<div
class="c7"
>
<div
Expand All @@ -1008,7 +1008,7 @@ exports[`PageLayout renders with dividers 1`] = `
>
Pane
</div>
</aside>
</div>
<footer
class="c10"
>
Expand Down
Loading