admin: only the content region scrolls on wide screens

the shell grid grew past the viewport and scrolled the document,
carrying the sidebar with it. the shell is now viewport-height at the
wide breakpoint with main as the sole scroll container; the nav list
scrolls inside the pinned rail; router scroll restoration targets the
inner scroller so navigation resets it and back/forward restores it.
This commit is contained in:
2026-08-23 15:17:51 +02:00
parent fe71efe335
commit f7f4c8be09
4 changed files with 46 additions and 2 deletions
+2
View File
@@ -425,6 +425,8 @@ export function createAppRouter(history?: RouterHistory, queryClient: QueryClien
context: { queryClient }, context: { queryClient },
defaultPreload: "intent", defaultPreload: "intent",
defaultPreloadStaleTime: 0, defaultPreloadStaleTime: 0,
scrollRestoration: true,
scrollToTopSelectors: ["#main-content"],
defaultPendingComponent: RoutePending, defaultPendingComponent: RoutePending,
defaultErrorComponent: RouteError, defaultErrorComponent: RouteError,
}); });
+9
View File
@@ -146,6 +146,15 @@ test("shell renders the overview route with all nav links", async () => {
} }
}); });
test("main carries the ids the router scrolls and restores", async () => {
renderShell();
await screen.findByRole("heading", { name: "Overview" });
const main = screen.getByRole("main");
expect(main.id).toBe("main-content");
expect(main.getAttribute("data-scroll-restoration-id")).toBe("main");
});
test("the three configuration pages sit under a labelled group, after the rest", async () => { test("the three configuration pages sit under a labelled group, after the rest", async () => {
renderShell(); renderShell();
await screen.findByRole("heading", { name: "Overview" }); await screen.findByRole("heading", { name: "Overview" });
+12 -2
View File
@@ -112,6 +112,8 @@ const styles = stylex.create({
}, },
shell: { shell: {
minHeight: "100dvh", minHeight: "100dvh",
height: { default: null, [WIDE]: "100dvh" },
overflow: { default: null, [WIDE]: "hidden" },
backgroundColor: colors.surface, backgroundColor: colors.surface,
color: colors.text, color: colors.text,
display: { default: "block", [WIDE]: "grid" }, display: { default: "block", [WIDE]: "grid" },
@@ -120,6 +122,8 @@ const styles = stylex.create({
sidebar: { sidebar: {
display: { default: "none", [WIDE]: "flex" }, display: { default: "none", [WIDE]: "flex" },
flexDirection: { default: null, [WIDE]: "column" }, flexDirection: { default: null, [WIDE]: "column" },
minHeight: { default: null, [WIDE]: 0 },
overflow: { default: null, [WIDE]: "hidden" },
borderRightWidth: 1, borderRightWidth: 1,
borderRightStyle: "solid", borderRightStyle: "solid",
borderRightColor: colors.border, borderRightColor: colors.border,
@@ -133,11 +137,14 @@ const styles = stylex.create({
}, },
sidebarNav: { sidebarNav: {
flex: 1, flex: 1,
minHeight: { default: null, [WIDE]: 0 },
overflowY: { default: null, [WIDE]: "auto" },
paddingInline: "0.5rem", paddingInline: "0.5rem",
}, },
column: { column: {
display: "flex", display: "flex",
minHeight: "100dvh", minHeight: { default: "100dvh", [WIDE]: 0 },
minWidth: { default: null, [WIDE]: 0 },
flexDirection: "column", flexDirection: "column",
}, },
header: { header: {
@@ -177,6 +184,9 @@ const styles = stylex.create({
}, },
main: { main: {
flex: 1, flex: 1,
minHeight: { default: null, [WIDE]: 0 },
minWidth: { default: null, [WIDE]: 0 },
overflowY: { default: null, [WIDE]: "auto" },
padding: "1rem", padding: "1rem",
}, },
}); });
@@ -335,7 +345,7 @@ export default function AppShell() {
<SidebarFooter /> <SidebarFooter />
</div> </div>
)} )}
<main {...stylex.props(styles.main)}> <main id="main-content" data-scroll-restoration-id="main" {...stylex.props(styles.main)}>
<Outlet /> <Outlet />
</main> </main>
</div> </div>
+23
View File
@@ -60,3 +60,26 @@ if (globalThis.CSS === undefined) {
* than per call; a test that genuinely never resolves still fails, only later. * than per call; a test that genuinely never resolves still fails, only later.
*/ */
configure({ asyncUtilTimeout: 5000 }); configure({ asyncUtilTimeout: 5000 });
/**
* jsdom implements no `Element.prototype.scrollTo`, and its `window.scrollTo` is
* a stub that logs "Not implemented". The router's scroll restoration calls both
* on every navigation, so without these the shell throws into its error boundary
* in tests while working in a browser. jsdom has no layout, so a scroll is a
* position assignment and nothing more.
*/
if (typeof Element.prototype.scrollTo !== "function") {
Element.prototype.scrollTo = function scrollTo(...args: unknown[]) {
const options = (typeof args[0] === "object" ? args[0] : { left: args[0], top: args[1] }) as ScrollToOptions;
if (typeof options.top === "number") this.scrollTop = options.top;
if (typeof options.left === "number") this.scrollLeft = options.left;
} as typeof Element.prototype.scrollTo;
}
window.scrollTo = function scrollTo(...args: unknown[]) {
const options = (typeof args[0] === "object" ? args[0] : { left: args[0], top: args[1] }) as ScrollToOptions;
if (typeof options.top === "number")
Object.defineProperty(window, "scrollY", { value: options.top, configurable: true });
if (typeof options.left === "number")
Object.defineProperty(window, "scrollX", { value: options.left, configurable: true });
} as typeof window.scrollTo;