-
Notifications
You must be signed in to change notification settings - Fork 992
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Steps towards a11y for Redwood Router (#1817)
* feat: announce page and scroll to top * fix: update yarn.lock * fix: update typing and test * feat: return early in getAnnouncement * fix: update resetNamedRoutes intest * fix: update set test to include route announcement
- Loading branch information
Showing
12 changed files
with
378 additions
and
5 deletions.
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
packages/cli/src/commands/generate/layout/__tests__/fixtures/withSkipLinkLayout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { SkipNavLink, SkipNavContent } from '@redwoodjs/router' | ||
import '@reach/skip-nav/styles.css' | ||
|
||
/** | ||
* since the main content isn't usually the first thing in the document, | ||
* it's important to provide a shortcut for keyboard and screen-reader users to skip to the main content | ||
* API docs: https://reach.tech/skip-nav/#reach-skip-nav | ||
*/ | ||
|
||
const A11yLayout: React.FunctionComponent = ({ children }) => { | ||
return ( | ||
<> | ||
{/* renders a link that remains hidden till focused; put it at the top of your layout */} | ||
<SkipNavLink /> | ||
<nav></nav> | ||
{/* renders a div as the target for the link; put it next to your main content */} | ||
<SkipNavContent /> | ||
<main>{children}</main> | ||
</> | ||
) | ||
} | ||
|
||
export default A11yLayout |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
packages/cli/src/commands/generate/layout/templates/layout.tsx.a11yTemplate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { SkipNavLink, SkipNavContent } from '@redwoodjs/router' | ||
import '@reach/skip-nav/styles.css' | ||
|
||
/** | ||
* since the main content isn't usually the first thing in the document, | ||
* it's important to provide a shortcut for keyboard and screen-reader users to skip to the main content | ||
* API docs: https://reach.tech/skip-nav/#reach-skip-nav | ||
*/ | ||
|
||
const ${singularPascalName}Layout: React.FunctionComponent = ({ children }) => { | ||
return ( | ||
<> | ||
{/* renders a link that remains hidden till focused; put it at the top of your layout */} | ||
<SkipNavLink /> | ||
<nav></nav> | ||
{/* renders a div as the target for the link; put it next to your main content */} | ||
<SkipNavContent /> | ||
<main>{children}</main> | ||
</> | ||
) | ||
} | ||
|
||
export default ${singularPascalName}Layout |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import { render, waitFor, act } from '@testing-library/react' | ||
import '@testing-library/jest-dom/extend-expect' | ||
|
||
import { Router, Route, navigate, routes, getAnnouncement } from '../internal' | ||
import RouteAnnouncement from '../route-announcement' | ||
|
||
// SETUP | ||
const HomePage = () => <h1>Home Page</h1> | ||
|
||
const RouteAnnouncementPage = () => ( | ||
<html> | ||
<head> | ||
<title>title content</title> | ||
</head> | ||
<body> | ||
<h1>RouteAnnouncement Page </h1> | ||
<RouteAnnouncement visuallyHidden> | ||
RouteAnnouncement content | ||
</RouteAnnouncement> | ||
<main>main content</main> | ||
</body> | ||
</html> | ||
) | ||
|
||
const H1Page = () => ( | ||
<html> | ||
<head> | ||
<title>title content</title> | ||
</head> | ||
<body> | ||
<h1>H1 Page</h1> | ||
<main>main content</main> | ||
</body> | ||
</html> | ||
) | ||
|
||
const NoH1Page = () => ( | ||
<html> | ||
<head> | ||
<title>title content</title> | ||
</head> | ||
<body> | ||
<div>NoH1 Page</div> | ||
<main>main content</main> | ||
</body> | ||
</html> | ||
) | ||
|
||
const NoH1OrTitlePage = () => ( | ||
<html> | ||
<head></head> | ||
<body> | ||
<div>NoH1OrTitle Page</div> | ||
<main>main content</main> | ||
</body> | ||
</html> | ||
) | ||
|
||
const EmptyH1Page = () => ( | ||
<html> | ||
<head> | ||
<title>title content</title> | ||
</head> | ||
<body> | ||
<h1></h1> | ||
<main>Empty H1 Page</main> | ||
</body> | ||
</html> | ||
) | ||
|
||
beforeEach(() => { | ||
window.history.pushState({}, null, '/') | ||
Object.keys(routes).forEach((key) => delete routes[key]) | ||
}) | ||
|
||
test('route announcer renders with aria-live="assertive" and role="alert"', async () => { | ||
const TestRouter = () => ( | ||
<Router> | ||
<Route path="/" page={HomePage} name="home" /> | ||
</Router> | ||
) | ||
|
||
const screen = render(<TestRouter />) | ||
|
||
await waitFor(() => { | ||
screen.getByText(/Home Page/i) | ||
const routeAnnouncer = screen.getByRole('alert') | ||
const ariaLiveValue = routeAnnouncer.getAttribute('aria-live') | ||
const roleValue = routeAnnouncer.getAttribute('role') | ||
expect(ariaLiveValue).toBe('assertive') | ||
expect(roleValue).toBe('alert') | ||
}) | ||
}) | ||
|
||
test('gets the announcement in the correct order of priority', async () => { | ||
const TestRouter = () => ( | ||
<Router> | ||
<Route path="/" page={RouteAnnouncementPage} name="routeAnnouncement" /> | ||
<Route path="/h1" page={H1Page} name="h1" /> | ||
<Route path="/noH1" page={NoH1Page} name="noH1" /> | ||
<Route path="/noH1OrTitle" page={NoH1OrTitlePage} name="noH1OrTitle" /> | ||
</Router> | ||
) | ||
|
||
const screen = render(<TestRouter />) | ||
|
||
// starts on route announcement. | ||
// since there's a RouteAnnouncement, it should announce that. | ||
await waitFor(() => { | ||
screen.getByText(/RouteAnnouncement Page/i) | ||
expect(getAnnouncement()).toBe('RouteAnnouncement content') | ||
}) | ||
|
||
// navigate to h1 | ||
// since there's no RouteAnnouncement, it should announce the h1. | ||
act(() => navigate(routes.h1())) | ||
await waitFor(() => { | ||
screen.getByText(/H1 Page/i) | ||
expect(getAnnouncement()).toBe('H1 Page') | ||
}) | ||
|
||
// navigate to noH1. | ||
// since there's no h1, it should announce the title. | ||
act(() => navigate(routes.noH1())) | ||
await waitFor(() => { | ||
screen.getByText(/NoH1 Page/i) | ||
expect(getAnnouncement()).toBe('title content') | ||
}) | ||
|
||
// navigate to noH1OrTitle. | ||
// since there's no h1 or title, | ||
// it should announce the location. | ||
act(() => navigate(routes.noH1OrTitle())) | ||
await waitFor(() => { | ||
screen.getByText(/NoH1OrTitle Page/i) | ||
expect(getAnnouncement()).toBe('new page at /noH1OrTitle') | ||
}) | ||
}) | ||
|
||
test('getAnnouncement handles empty PageHeader', async () => { | ||
const TestRouter = () => ( | ||
<Router> | ||
<Route path="/" page={EmptyH1Page} name="emptyH1" /> | ||
</Router> | ||
) | ||
|
||
const screen = render(<TestRouter />) | ||
|
||
await waitFor(() => { | ||
screen.getByText(/Empty H1 Page/i) | ||
expect(getAnnouncement()).toBe('title content') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.