mAi: #85 - sidebar scroll position persists across nav
Sidebar nav clicks trigger a full page reload, which rebuilds the sidebar from scratch and snaps .sidebar-nav back to scrollTop=0. Persist scrollTop to sessionStorage (paliad.sidebar.scroll) on every scroll and restore on initSidebar(). Re-apply once after /api/user-views resolves so the async layout shift doesn't leave the user a few rows off. sessionStorage scopes the value to the tab: Cmd-click / right-click "open in new tab" still produces a fresh tab that starts at the top.
This commit is contained in:
@@ -11,6 +11,13 @@ const WIDTH_KEY = "paliad-sidebar-width";
|
||||
const SIDEBAR_WIDTH_MIN = 180;
|
||||
const SIDEBAR_WIDTH_MAX = 480;
|
||||
const SIDEBAR_WIDTH_DEFAULT = 240;
|
||||
// Per-tab scroll position of the .sidebar-nav scroll container. Persisted
|
||||
// on every scroll event, restored on initSidebar() so a full-page nav
|
||||
// click doesn't bounce the user back to the top of a long sidebar
|
||||
// (Werkzeuge + projects + user views can easily overflow). sessionStorage
|
||||
// scopes it to the tab — opening a sidebar link in a new tab (Cmd-click)
|
||||
// starts that tab fresh at the top, which matches user expectation.
|
||||
const SCROLL_KEY = "paliad.sidebar.scroll";
|
||||
|
||||
// toggleMobileSidebar opens or closes the slide-out drawer. Exposed so the
|
||||
// BottomNav menu slot can call it without duplicating the open/close
|
||||
@@ -49,6 +56,23 @@ function applySidebarWidth(px: number): void {
|
||||
document.documentElement.style.setProperty("--sidebar-width", `${px}px`);
|
||||
}
|
||||
|
||||
// readStoredScroll returns the persisted scrollTop or 0 when missing /
|
||||
// malformed. Bounds are checked at apply time against the actual
|
||||
// scrollHeight, so a stale value pointing past the current scroll range
|
||||
// is harmless (the browser clamps assignments to [0, max]).
|
||||
function readStoredScroll(): number {
|
||||
const raw = sessionStorage.getItem(SCROLL_KEY);
|
||||
if (raw === null) return 0;
|
||||
const n = parseInt(raw, 10);
|
||||
if (!Number.isFinite(n) || n < 0) return 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
function applySidebarScroll(nav: HTMLElement, px: number): void {
|
||||
if (px <= 0) return;
|
||||
nav.scrollTop = px;
|
||||
}
|
||||
|
||||
// migrateLegacyPinKey copies the pre-rebrand pin state into the new key on
|
||||
// first load and removes the stale entry. Drop this fallback once the rename
|
||||
// grace period is over.
|
||||
@@ -79,6 +103,7 @@ export function initSidebar() {
|
||||
const sidebar = document.querySelector<HTMLElement>(".sidebar");
|
||||
if (!sidebar) return;
|
||||
initSidebarResize(sidebar);
|
||||
initSidebarScrollRestore(sidebar);
|
||||
|
||||
const pinBtn = sidebar.querySelector<HTMLButtonElement>(".sidebar-pin");
|
||||
const hamburger = document.querySelector<HTMLButtonElement>(".sidebar-hamburger");
|
||||
@@ -293,6 +318,29 @@ function initSidebarResize(sidebar: HTMLElement): void {
|
||||
});
|
||||
}
|
||||
|
||||
// initSidebarScrollRestore wires the .sidebar-nav scroll container to
|
||||
// sessionStorage so the user's scroll position survives a full-page
|
||||
// navigation (every sidebar link click is a real reload — see m/paliad#85).
|
||||
// Restore is synchronous on init so the first paint is already at the
|
||||
// right offset; the passive scroll listener persists subsequent moves.
|
||||
// reapplySidebarScroll() exists so callers that mutate sidebar content
|
||||
// async (initUserViewsGroup appending /api/user-views into the Ansichten
|
||||
// group) can nudge the scroll back to where it was after the layout shift.
|
||||
function initSidebarScrollRestore(sidebar: HTMLElement): void {
|
||||
const nav = sidebar.querySelector<HTMLElement>(".sidebar-nav");
|
||||
if (!nav) return;
|
||||
applySidebarScroll(nav, readStoredScroll());
|
||||
nav.addEventListener("scroll", () => {
|
||||
sessionStorage.setItem(SCROLL_KEY, String(nav.scrollTop));
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
function reapplySidebarScroll(): void {
|
||||
const nav = document.querySelector<HTMLElement>(".sidebar .sidebar-nav");
|
||||
if (!nav) return;
|
||||
applySidebarScroll(nav, readStoredScroll());
|
||||
}
|
||||
|
||||
// Changelog badge — fetches the count of entries newer than the locally
|
||||
// stored "last seen" stamp and renders a dot + number on the Neuigkeiten
|
||||
// link. Skipped on the changelog page itself because changelog.ts stamps
|
||||
@@ -432,6 +480,11 @@ function initUserViewsGroup(): void {
|
||||
for (const view of views) {
|
||||
items.appendChild(renderUserViewItem(view, currentPath));
|
||||
}
|
||||
// The synchronous restore in initSidebarScrollRestore() happened
|
||||
// before these views were appended, so a saved scrollTop that
|
||||
// pointed below the Ansichten group would now sit on the wrong
|
||||
// row. Re-apply once the layout has stabilised.
|
||||
reapplySidebarScroll();
|
||||
// After rendering, kick off count refresh for views that opted in.
|
||||
for (const view of views) {
|
||||
if (view.show_count) {
|
||||
|
||||
Reference in New Issue
Block a user