Skip to content

Instantly share code, notes, and snippets.

@chaance
Last active August 9, 2021 18:09
Show Gist options
  • Save chaance/ea4eb4cc9e9836e4d63df1506c53c9a9 to your computer and use it in GitHub Desktop.
Save chaance/ea4eb4cc9e9836e4d63df1506c53c9a9 to your computer and use it in GitHub Desktop.
More accessible client-side routing with React Router. Experimental code!
function useAccessibleRouting(skipLinkRef) {
let location = useLocation();
let liveRegionRef = React.useRef();
React.useEffect(
/**
* Create a live region that will be used to announce route changes when the
* user navigates. This hook should be called from the root App component,
* so this should be created and appended to the DOM only once.
*/
() => {
let liveRegion = document.createElement("div");
liveRegion.classList.add("visually-hidden");
liveRegion.setAttribute("role", "status");
liveRegion.id = ROUTE_REGION_ID;
document.body.appendChild(liveRegion);
liveRegionRef.current = liveRegion;
},
[]
);
useUpdateEffect(
/**
* When the location state changes we:
* - Update the live region we created on the first render
* - Look for a skip-link on the next page (see `global-skip-link`)
* - If we find a skip-link, focus it without changing the scroll position.
* Keyboard users should be able to navigate from the focused element,
* but client-side route changes _probably_ shouldn't change the scroll
* position by default. If we want a specific transition to update the
* scroll position we can handle that on a route-by-route basis.
*/
() => {
let liveRegion = liveRegionRef.current;
let skipLink = skipLinkRef.current;
// Routes should expose a <meta name="human-title" /> element with a
// content attribute that tell us the name of this page. Title tags aren't
// ideal because they are probably going to include some SEO cruft.
let humanTitleElem = document.querySelector("meta[name='human-title']");
let titleElem = document.querySelector("title");
let pageName = humanTitleElem
? humanTitleElem.getAttribute("content")
: // If for whatever reason we don't have a human-title meta tag, we'll
// use the title element and try to get the page name from it.
titleElem
? titleElem.innerText.split("|")[0].trim()
: // Final fallback will be to assume the page name from the path, but I
// don't think we'll end up here realistically but whatever.
location.pathname.split("/")[1] + " page";
// Do not move focus if the new location has a hash; the ID of the element
// that matches the hash should be focused. Let the browser deal with it.
if (skipLink && !location.hash) {
focusWithoutScrolling(skipLink);
}
// Update live region to announce the route change
liveRegion.textContent = `Navigated to ${pageName}`;
},
[location]
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment