Dealing with an unusual focus with a React Provider
Of course, in most cases, a valid DOM and correct ordering of elements should allow for effortless focussing as you move through your browser’s user interface. But there are definitely scenarios where this can’t be the case and you need a good programmatic way to focus interactive elements:
- Maybe you have some Hot Keys defined which let’s your user move to a form
- Perhaps some parts of your user interface are located within a React Portal and therefore in a completely different place in your DOM hierarchy
- Or you open a sidebar, a modal or akin and want to direct focus for better accessibility
And for React, even in 2024, focussing an element is it still means getting your hands dirty with the DOM itself — in React language, use ref.
For a large-scale application with a React Frontend, we recently introduced a FocusManager. Under the hood a React Context to manage focus for a coherent part of the UI.
It consists of 3 parts
The FocusManager Context
import React from "react";
type FocusContextType<K> = {
setFocus: (key: K) => void;
registerFocusElem: (key: K, focusOptions?: FocusOptions) => void;
};
type FocusOptions = { focusOnRegister?: boolean };
const FocusContext = React.createContext<FocusContextType<any> | undefined>(
undefined
);
export function FocusManager<K>(props: React.PropsWithChildren<{}>) {
const refs = React.useRef<Map<K, React.RefObject<HTMLElement>>>(new Map());
const setFocus = (key: K) => {
const ref = refs.current.get(key);
if (!ref || !ref.current) {
return; // maybe print some warning here?
}
ref.current.focus();
};
const registerFocusElem = (key: K, focusOptions?: FocusOptions) => {
return (ref: HTMLInputElement | HTMLButtonElement) => {
if (refs.current.has(key)) {
return;
}
refs.current.set(key, { current: ref });
if (focusOptions?.focusOnRegister) {
ref.focus();
}
};
};
return (
<FocusContext.Provider value={{ setFocus, registerFocusElem }}>
{props.children}
</FocusContext.Provider>
);
}
export function useFocusContext<K>() {
const context = React.useContext<FocusContextType<K> | undefined>(
FocusContext
);
if (context === undefined) {
throw new Error(
"useFocusContext must be used within a FocusManagerProvider"
);
}
return context;
}
Register a ref by name
import { useFocusContext } from "./FocusManager";
export function Component1() {
const { registerFocusElem } = useFocusContext();
return <input ref={registerFocusElem("input1")}></input>;
}
Event based focus
import { useFocusContext } from "./FocusManager";
export function Buttons() {
const { setFocus } = useFocusContext();
return (
<button onClick={() => setFocus("input1")}>Focus Input1</button>
);
}
That’s it. Perhaps this will inspire you to improve the focus of your applications so that your users enjoy it even more.
Here is a Codesandbox with an exemplary implementation. Thanks for reading 🙌