Skip to content

Commit

Permalink
Handle focus on route change (#2321)
Browse files Browse the repository at this point in the history
* feat: focus RouteFocus on route change

* feat: handle elements that aren't focusable

* feat: add some tests for getFocus

* style: split up tests
  • Loading branch information
jtoar committed Apr 22, 2021
1 parent 930eda7 commit 99575c3
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 1 deletion.
133 changes: 133 additions & 0 deletions packages/router/src/__tests__/route-focus.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { render, waitFor } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect'

import { Router, Route, routes, getFocus } from '../internal'
import RouteFocus from '../route-focus'

// SETUP
const RouteFocusPage = () => (
<>
<RouteFocus>
<a>a link is a focusable element</a>
</RouteFocus>
<h1>Route Focus Page</h1>
<p></p>
</>
)

const NoRouteFocusPage = () => <h1>No Route Focus Page</h1>

const RouteFocusNoChildren = () => (
<>
<RouteFocus></RouteFocus>
<h1>Route Focus No Children Page</h1>
<p></p>
</>
)

const RouteFocusTextNodePage = () => (
<>
<RouteFocus>some text</RouteFocus>
<h1>Route Focus Text Node Page </h1>
<p></p>
</>
)

const RouteFocusNegativeTabIndexPage = () => (
<>
<RouteFocus>
<p>my tabindex is -1</p>
</RouteFocus>
<h1>Route Focus Negative Tab Index Page </h1>
<p></p>
</>
)

beforeEach(() => {
window.history.pushState({}, null, '/')
Object.keys(routes).forEach((key) => delete routes[key])
})

test('getFocus returns a focusable element if RouteFocus has one', async () => {
const TestRouter = () => (
<Router>
<Route path="/" page={RouteFocusPage} name="routeFocus" />
</Router>
)

const screen = render(<TestRouter />)

await waitFor(() => {
screen.getByText(/Route Focus Page/i)
const routeFocus = getFocus()
expect(routeFocus).toHaveTextContent('a link is a focusable element')
})
})

test("getFocus returns null if there's no RouteFocus", async () => {
const TestRouter = () => (
<Router>
<Route path="/" page={NoRouteFocusPage} name="noRouteFocus" />
</Router>
)

const screen = render(<TestRouter />)

await waitFor(() => {
screen.getByText(/No Route Focus Page/i)
const routeFocus = getFocus()
expect(routeFocus).toBeNull()
})
})

test("getFocus returns null if RouteFocus doesn't have any children", async () => {
const TestRouter = () => (
<Router>
<Route path="/" page={RouteFocusNoChildren} name="routeFocusNoChildren" />
</Router>
)

const screen = render(<TestRouter />)

await waitFor(() => {
screen.getByText(/Route Focus No Children Page/i)
const routeFocus = getFocus()
expect(routeFocus).toBeNull()
})
})

test('getFocus returns null if RouteFocus has just a Text node', async () => {
const TestRouter = () => (
<Router>
<Route path="/" page={RouteFocusTextNodePage} name="routeFocusTextNode" />
</Router>
)

const screen = render(<TestRouter />)

await waitFor(() => {
screen.getByText(/Route Focus Text Node Page/i)
const routeFocus = getFocus()
expect(routeFocus).toBeNull()
})
})

test("getFocus returns null if RouteFocus's child isn't focusable", async () => {
const TestRouter = () => (
<Router>
<Route
path="/"
page={RouteFocusNegativeTabIndexPage}
name="routeFocusNegativeTabIndex"
/>
</Router>
)

const screen = render(<TestRouter />)

await waitFor(() => {
screen.getByText(/Route Focus Negative Tab Index Page/i)
const routeFocus = getFocus()
expect(routeFocus).toBeNull()
})
})
2 changes: 2 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export { usePageLoadingContext } from './page-loader'

export { default as RouteAnnouncement } from './route-announcement'
export * from './route-announcement'
export { default as RouteFocus } from './route-focus'
export * from './route-focus'

/**
* A more specific interface will be generated by
Expand Down
14 changes: 13 additions & 1 deletion packages/router/src/page-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import React, { useContext } from 'react'

import isEqual from 'lodash.isequal'

import { createNamedContext, Spec, getAnnouncement } from './internal'
import {
createNamedContext,
Spec,
getAnnouncement,
getFocus,
resetFocus,
} from './internal'

export interface PageLoadingContextInterface {
loading: boolean
Expand Down Expand Up @@ -93,6 +99,12 @@ export class PageLoader extends React.Component<Props> {
if (this.announcementRef.current) {
this.announcementRef.current.innerText = getAnnouncement()
}
const routeFocus = getFocus()
if (!routeFocus) {
resetFocus()
} else {
routeFocus.focus()
}
}
}

Expand Down
18 changes: 18 additions & 0 deletions packages/router/src/route-focus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react'

/**
* this initial implementation borrows (heavily!) from madalyn's great work at gatsby:
* - issue: https://github.com/gatsbyjs/gatsby/issues/21059
* - PR: https://github.com/gatsbyjs/gatsby/pull/26376
*/
const RouteFocus: React.FC<RouteFocusProps> = ({ children, ...props }) => (
<div {...props} data-redwood-route-focus={true}>
{children}
</div>
)

export interface RouteFocusProps {
children: React.ReactNode
}

export default RouteFocus
23 changes: 23 additions & 0 deletions packages/router/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,26 @@ export const getAnnouncement = () => {

return `new page at ${global?.location.pathname}`
}

export const getFocus = () => {
const routeFocus = global?.document.querySelectorAll(
'[data-redwood-route-focus]'
)?.[0]

if (
!routeFocus ||
!routeFocus.children.length ||
(routeFocus.children[0] as HTMLElement).tabIndex < 0
) {
return null
}

return routeFocus.children[0] as HTMLElement
}

// note: tried document.activeElement.blur(), but that didn't reset the focus flow
export const resetFocus = () => {
global?.document.body.setAttribute('tabindex', '-1')
global?.document.body.focus()
global?.document.body.removeAttribute('tabindex')
}

0 comments on commit 99575c3

Please sign in to comment.