Skip to content

Commit

Permalink
Add focus context for managing focus on route changes
Browse files Browse the repository at this point in the history
  • Loading branch information
cathryngriffiths committed May 20, 2020
1 parent da77adb commit 0dbef43
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 5 deletions.
26 changes: 26 additions & 0 deletions packages/react-router/src/components/FocusContext/FocusContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, {useRef, useEffect, ReactNode, createRef} from 'react';
import {FocusContext as Context} from '../../context';
import {useCurrentUrl} from '../../hooks';
import {Focusable} from '../../types';

export function FocusContext({children}: {children: ReactNode}) {
const currentUrl = useCurrentUrl();
const focusRef = createRef<Focusable>();
const focus = () => {
const target = focusRef.current ?? document.body;
target.focus();
};

const firstRender = useRef(true);

useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
focus();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUrl.pathname]);

return <Context.Provider value={focusRef}>{children}</Context.Provider>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {FocusContext} from './FocusContext';
3 changes: 2 additions & 1 deletion packages/react-router/src/components/Router/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {memo, useState, useEffect, useRef, useMemo} from 'react';
import {useSerialized} from '@quilted/react-html';

import {FocusContext} from '../FocusContext';
import {CurrentUrlContext, RouterContext} from '../../context';
import {Router as RouterControl, EXTRACT} from '../../router';
import {Prefetcher} from '../Prefetcher';
Expand Down Expand Up @@ -42,7 +43,7 @@ export const Router = memo(function Router({
<RouterContext.Provider value={routerRef.current}>
<CurrentUrlContext.Provider value={url}>
<Prefetcher />
{children}
<FocusContext>{children}</FocusContext>
</CurrentUrlContext.Provider>
</RouterContext.Provider>
</>
Expand Down
1 change: 1 addition & 0 deletions packages/react-router/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export {Redirect} from './Redirect';
export {Route} from './Route';
export {Router} from './Router';
export {Switch} from './Switch';
export {FocusContext} from './FocusContext';
6 changes: 4 additions & 2 deletions packages/react-router/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {createContext} from 'react';
import {EnhancedURL} from './types';
import {createContext, RefObject} from 'react';
import {EnhancedURL, Focusable} from './types';

export const CurrentUrlContext = createContext<EnhancedURL | null>(null);
export const RouterContext = createContext<import('./router').Router | null>(
null,
);
export const SwitchContext = createContext<{matched(): void} | null>(null);

export const FocusContext = createContext<RefObject<Focusable> | null>(null);
19 changes: 19 additions & 0 deletions packages/react-router/src/hooks/focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {useContext} from 'react';

import {FocusContext} from '../context';

function useFocusContext() {
const context = useContext(FocusContext);

if (context == null) {
throw new Error(
'You attempted to use the focus context, but none was found. Make sure your code is nested in a <Router />',
);
}

return context;
}

export function useRouteChangeFocusRef() {
return useFocusContext();
}
1 change: 1 addition & 0 deletions packages/react-router/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export {useCurrentUrl} from './url';
export {useRouter} from './router';
export {useNavigationBlock} from './navigation-block';
export {useRouteChangeFocusRef} from './focus';
7 changes: 6 additions & 1 deletion packages/react-router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ export {
Prefetcher,
NavigationBlock,
} from './components';
export {useCurrentUrl, useRouter, useNavigationBlock} from './hooks';
export {
useCurrentUrl,
useRouter,
useNavigationBlock,
useRouteChangeFocusRef,
} from './hooks';
export type {NavigateTo} from './router';
3 changes: 2 additions & 1 deletion packages/react-router/src/testing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {useRef, ReactNode} from 'react';

import {Router, NavigateTo, NavigateOptions, Options} from './router';
import {CurrentUrlContext, RouterContext} from './context';
import {FocusContext} from './components';

class TestRouterControl extends Router {
navigate(_to: NavigateTo, _options?: NavigateOptions) {}
Expand All @@ -26,7 +27,7 @@ export function TestRouter({children, router: initialRouter}: Props) {
return (
<RouterContext.Provider value={router.current}>
<CurrentUrlContext.Provider value={router.current.currentUrl}>
{children}
<FocusContext>{children}</FocusContext>
</CurrentUrlContext.Provider>
</RouterContext.Provider>
);
Expand Down
4 changes: 4 additions & 0 deletions packages/react-router/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ export interface Matcher {
}

export type Match = string | RegExp | Matcher;

export interface Focusable {
focus(): void;
}

0 comments on commit 0dbef43

Please sign in to comment.