Skip to content

Instantly share code, notes, and snippets.

@WestonThayer
Last active March 14, 2022 21:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save WestonThayer/c8e64a64c278154efb1975d87b68b3fd to your computer and use it in GitHub Desktop.
Save WestonThayer/c8e64a64c278154efb1975d87b68b3fd to your computer and use it in GitHub Desktop.
Basic Next.js focus restoration
// Browsers keep track of where your keyboard focus is when you click
// a link, and they restore focus to the same link when you navigate
// back (via https://web.dev/bfcache/)
//
// SPAs like Next.js break this paradigm because they handle routing
// themselves. This is a basic approach bringing the feature back. It
// doesn't handle restoring the view-state (well, Next.js handles
// scroll restoration out-of-the-box, but that's it) like re-opening
// dropdowns or dialogs or FAQ items that contained the clicked link.
import * as React from "react";
import Router from "next/router";
import finder from "@medv/finder";
const MyApp = (props) => {
React.useEffect(() => {
let previousActiveElementSelector = "";
let isGoingBack = false;
const handleRouteBack = () => {
isGoingBack = true;
return true; // Let Next.js continue to route
};
const handleRouteChangeStart = () => {
const activeElement = window.document.activeElement;
// Only cache the currently focused element if going forward
if (!isGoingBack && activeElement) {
// Can't simply cache document.activeElement because the
// Next.js Page containing it is about to be unmounted.
// After that, calling .focus() won't do anything, the
// element is no longer in the DOM.
//
// So instead, cache a unique CSS selector we can use to
// find it when the Page is remounted. @medv/finder is
// a fast little helper to calculate unique selectors.
// Hopefully doesn't become a performance bottleneck.
previousActiveElementSelector = finder(activeElement);
}
};
const handleRouteChangeComplete = () => {
// HACK: this event fires before React has rendered the
// new page to the DOM. So delay just a bit, there's
// gotta be a better hook though?
window.setTimeout(() => {
if (isGoingBack) {
// Restore focus to cached element
if (previousActiveElementSelector) {
const el = window.document.querySelector(
previousActiveElementSelector,
);
if (el && el.focus) {
el.focus();
}
}
} else {
// Going forward, maybe update a live-region or
// focus a heading?
}
isGoingBack = false;
}, 10);
};
// https://nextjs.org/docs/api-reference/next/router#routerbeforepopstate
Router.beforePopState(handleRouteBack);
// https://nextjs.org/docs/api-reference/next/router#routerevents
Router.events.on("routeChangeStart", handleRouteChangeStart);
Router.events.on("routeChangeComplete", handleRouteChangeComplete);
return () => {
Router.events.off("routeChangeStart", handleRouteChangeStart);
Router.events.off("routeChangeComplete", handleRouteChangeComplete)
};
});
return ...;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment