Last active
August 9, 2021 18:09
-
-
Save chaance/ea4eb4cc9e9836e4d63df1506c53c9a9 to your computer and use it in GitHub Desktop.
More accessible client-side routing with React Router. Experimental code!
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
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