Files
warmbly/docs/components/shared/sidebar-scroll-keeper.tsx
T

59 lines
2.0 KiB
TypeScript

'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
// Clicking a page in the sidebar keeps the sidebar where it was. Fumadocs scrolls
// the new active item into view, which moves a list you were just reading. The
// position is kept for a moment in session storage, so it also survives a full
// page load on a static host.
const KEY = 'wd-sidebar-scroll';
const FRESH_MS = 5000;
function viewport(): HTMLElement | null {
const root = document.getElementById('nd-sidebar');
if (!root) return null;
for (const el of root.querySelectorAll<HTMLElement>('*')) {
const y = getComputedStyle(el).overflowY;
if ((y === 'auto' || y === 'scroll') && el.scrollHeight > el.clientHeight) return el;
}
return null;
}
export function SidebarScrollKeeper() {
const pathname = usePathname();
useEffect(() => {
const remember = (e: MouseEvent) => {
const link = (e.target as Element | null)?.closest('#nd-sidebar a[href]');
const el = link && viewport();
if (el) sessionStorage.setItem(KEY, JSON.stringify({ top: el.scrollTop, at: Date.now() }));
};
document.addEventListener('click', remember, true);
return () => document.removeEventListener('click', remember, true);
}, []);
useEffect(() => {
let saved: { top: number; at: number } | null = null;
try {
saved = JSON.parse(sessionStorage.getItem(KEY) ?? 'null');
} catch {}
if (!saved || Date.now() - saved.at > FRESH_MS) return;
// The active item scrolls itself into view after this render; put the list back after it.
const restore = () => {
const el = viewport();
if (el && saved) el.scrollTop = saved.top;
};
restore();
const frame = requestAnimationFrame(restore);
const late = window.setTimeout(restore, 80);
const done = window.setTimeout(() => sessionStorage.removeItem(KEY), 400);
return () => {
cancelAnimationFrame(frame);
window.clearTimeout(late);
window.clearTimeout(done);
};
}, [pathname]);
return null;
}