diff --git a/src/lib/client/scroll-restore.ts b/src/lib/client/scroll-restore.ts new file mode 100644 index 0000000..e923d3a --- /dev/null +++ b/src/lib/client/scroll-restore.ts @@ -0,0 +1,76 @@ +// Persistent scroll restoration across client navigations. +// +// SvelteKit only restores scroll synchronously after the new page mounts. +// Pages whose content is fetched in onMount/afterNavigate (e.g. home, +// wishlist, shopping-list) are still empty at that point, so the saved +// scrollY can't be reached and the browser clamps to 0. +// +// We patch this by saving scrollY on beforeNavigate and re-applying it +// after popstate as soon as the document is tall enough — using rAF +// polling with a hard time budget so we never spin. + +const STORAGE_KEY = 'kochwas:scroll'; +const POLL_BUDGET_MS = 1500; +const MIN_RESTORE_Y = 40; // ignore noise: don't override a default top scroll + +type ScrollMap = Record; + +function readMap(): ScrollMap { + if (typeof sessionStorage === 'undefined') return {}; + try { + const raw = sessionStorage.getItem(STORAGE_KEY); + return raw ? (JSON.parse(raw) as ScrollMap) : {}; + } catch { + return {}; + } +} + +function writeMap(map: ScrollMap): void { + if (typeof sessionStorage === 'undefined') return; + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(map)); + } catch { + // quota exceeded — silently drop, scroll memory is best-effort + } +} + +function urlKey(): string { + if (typeof location === 'undefined') return ''; + return location.pathname + location.search; +} + +export function recordScroll(): void { + if (typeof window === 'undefined') return; + const key = urlKey(); + if (!key) return; + const map = readMap(); + map[key] = window.scrollY; + writeMap(map); +} + +export function restoreScroll(navType: string | null | undefined): void { + if (typeof window === 'undefined') return; + if (navType !== 'popstate') return; + const key = urlKey(); + if (!key) return; + const target = readMap()[key]; + if (!target || target < MIN_RESTORE_Y) return; + + const start = performance.now(); + const step = () => { + const docHeight = document.documentElement.scrollHeight; + const reachable = Math.max(0, docHeight - window.innerHeight); + if (reachable >= target - 4) { + window.scrollTo({ top: target, left: 0, behavior: 'instant' }); + return; + } + if (performance.now() - start >= POLL_BUDGET_MS) { + // Best effort — content never grew tall enough; clamp will land us + // at the bottom of what's available. + window.scrollTo({ top: target, left: 0, behavior: 'instant' }); + return; + } + requestAnimationFrame(step); + }; + requestAnimationFrame(step); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 51ff701..e675128 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,7 @@