Pages, routes and titles
A single-page app has to do what a page load does.
A single-page app has to do what a page load does. After a router navigation, nothing moves focus or
announces the new page. Update document.title (the Title component of @shadow-js/web, or a route's
head), and move focus to the new page's heading or announce it. The route announcer (routeAnnouncer() from
@shadow-js/router/announcer, on by default in Dusk) does both for you: it announces the title, falls back to the
first h1 and then the path, and moves focus to the element a URL hash names. The example below does it by hand:
src/route-focus.tsxtsx
import { useEffect } from "@shadow-js/core";
import {
createMemoryHistory,
createRouter,
defineRoute,
Outlet,
RouterProvider,
useLocation,
} from "@shadow-js/router";
function RouteFocus() {
const location = useLocation();
// `defer` skips the first run: the browser already handles the first page load.
useEffect(
() => location.pathname,
() => {
document.querySelector<HTMLElement>("main h1")?.focus();
},
{ defer: true },
);
return null;
}
function Page(props: { title: string }) {
return (
<main>
<h1 tabIndex={-1}>{props.title}</h1>
</main>
);
}
const home = defineRoute("/", { component: () => <Page title="Home" /> });
const about = defineRoute("/about", { component: () => <Page title="About" /> });
export function createAppRouter() {
return createRouter({
routes: [home, about],
history: createMemoryHistory({ initialEntries: ["/"] }),
});
}
export function App(props: { router: ReturnType<typeof createAppRouter> }) {
return (
<RouterProvider router={props.router}>
<RouteFocus />
<Outlet />
</RouterProvider>
);
}
A tabIndex={-1} heading can be focused by script without joining the tab order.