docs(ui): center the active nav link in the sidebar instead of edge-clamping

The clamp put deep-linked highlights flush against the sidebar's bottom
edge. Centering is simpler AND handles the max-scroll case for free:
scrollTop carries the item's center to the scrollport center, and the
browser clamps to the scroll extent — items near the nav's top/bottom
land as centered as the content allows (scroll pinned fully up/down),
which is the requested fallback.
This commit is contained in:
John O'Keefe committed 2026-10-24 22:04:06 -04:00
1 parent da0bae94bb
commit dd2ab4ea96
1 file changed
+14 -10
+14 -10
View File
@@ -45,23 +45,27 @@ function scrollSidebarToActive(): void {
const sidebar = document.getElementById("docs-sidebar"); const sidebar = document.getElementById("docs-sidebar");
if (!sidebar) return; if (!sidebar) return;
// Skip the off-canvas mobile drawer (translated off screen). // The class list ALWAYS carries -translate-x-full on desktop too
if (sidebar.classList.contains("-translate-x-full")) return; // (lg:translate-x-0 overrides it only at the media-query level), so
// classList can't tell viewports apart — match the tailwind lg
// breakpoint explicitly: below it the sidebar is the off-canvas drawer.
if (!window.matchMedia("(min-width: 1024px)").matches) return;
const active = sidebar.querySelector<HTMLAnchorElement>( const active = sidebar.querySelector<HTMLAnchorElement>(
`a[href="${location.pathname}"], a[href="${location.pathname.replace(/\.md$/, "")}"]`, `a[href="${location.pathname}"], a[href="${location.pathname.replace(/\.md$/, "")}"]`,
); );
if (!active) return; if (!active) return;
// Run once the browser has applied styles and laid the sidebar out.
requestAnimationFrame(() => {
const sb = sidebar.getBoundingClientRect(); const sb = sidebar.getBoundingClientRect();
const ar = active.getBoundingClientRect(); const ar = active.getBoundingClientRect();
if (ar.top >= sb.top && ar.bottom <= sb.bottom) return; // already visible // Center the active item in the sidebar's visible area. The browser
// clamps scrollTop to the scroll extent, so items near the top or
if (ar.top < sb.top) { // bottom of the nav land "as centered as possible" (scroll pinned all
sidebar.scrollTop += ar.top - sb.top; // the way up/down) — no manual edge-clamping needed.
} else { sidebar.scrollTop += ar.top - sb.top - sb.height / 2 + ar.height / 2;
sidebar.scrollTop += ar.bottom - sb.bottom; });
}
} }
let docsSearchTimeout: ReturnType<typeof setTimeout> | null = null; let docsSearchTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -153,5 +157,5 @@ Alpine.data("docs", () => ({
toggleCollapsible, toggleCollapsible,
initializeSearch: initializeDocsSearch, initializeSearch: initializeDocsSearch,
initializeCodeCopyButtons, initializeCodeCopyButtons,
scrollSidebarToActive, scrollSidebarToActive: () => requestAnimationFrame(scrollSidebarToActive),
})); }));