From 420742e7c9f7b6c3c7944d6f3335c6d35b95bd1b Mon Sep 17 00:00:00 2001 From: Darius Cepulis Date: Wed, 25 Feb 2026 14:29:54 -0600 Subject: [PATCH] feat(site): preserve scroll position on framework switch (pagereveal) (#608) Co-authored-by: Claude Opus 4.6 --- .../docs/DocsSidebarRestoration.astro | 62 ++++++++++++------- site/src/components/docs/Selectors.tsx | 14 ++++- .../installation/JSPickerClient.tsx | 15 ++++- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/site/src/components/docs/DocsSidebarRestoration.astro b/site/src/components/docs/DocsSidebarRestoration.astro index bd4d4201..cea274bf 100644 --- a/site/src/components/docs/DocsSidebarRestoration.astro +++ b/site/src/components/docs/DocsSidebarRestoration.astro @@ -15,39 +15,55 @@ const { docsSidebarId } = Astro.props; const STORAGE_KEY = 'vjs-sidebar-state'; window.addEventListener('pagereveal', () => { + // Restore sidebar state const aside = document.getElementById(docsSidebarId); - if (!aside) return; - const stored = sessionStorage.getItem(STORAGE_KEY); + if (aside) { + const stored = sessionStorage.getItem(STORAGE_KEY); - if (stored) { - try { - const state = JSON.parse(stored); + if (stored) { + try { + const state = JSON.parse(stored); - if (state.sidebarScroll !== undefined) { - aside.scrollTop = state.sidebarScroll; + if (state.sidebarScroll !== undefined) { + aside.scrollTop = state.sidebarScroll; + } + + if (state.detailsState) { + document.querySelectorAll(`#${docsSidebarId} details[id]`).forEach((details) => { + const savedState = state.detailsState[details.id]; + if (savedState !== undefined) { + details.open = savedState; + } + }); + } + } catch (e) { + console.error('[Sidebar] Failed to restore state', e); } + } - if (state.detailsState) { - document.querySelectorAll(`#${docsSidebarId} details[id]`).forEach((details) => { - const savedState = state.detailsState[details.id]; - if (savedState !== undefined) { - details.open = savedState; - } - }); + // Auto-expand sections containing active link (override saved state) + const activeLink = aside.querySelector('a[aria-current="page"]'); + if (activeLink) { + let parent = activeLink.closest('details'); + while (parent && aside.contains(parent)) { + parent.open = true; + parent = parent.parentElement?.closest('details') ?? null; } - } catch (e) { - console.error('[Sidebar] Failed to restore state', e); } } - // Auto-expand sections containing active link (override saved state) - const activeLink = aside.querySelector('a[aria-current="page"]'); - if (activeLink) { - let parent = activeLink.closest('details'); - while (parent && aside.contains(parent)) { - parent.open = true; - parent = parent.parentElement?.closest('details') ?? null; + // Restore page scroll position (set by framework switch) + try { + const scrollData = sessionStorage.getItem('vjs-page-scroll'); + if (scrollData) { + sessionStorage.removeItem('vjs-page-scroll'); + const { url, scrollY } = JSON.parse(scrollData); + if (url.replace(/\/$/, '') === window.location.pathname.replace(/\/$/, '')) { + window.scrollTo(0, scrollY); + } } + } catch (e) { + // Ignore storage errors } }); diff --git a/site/src/components/docs/Selectors.tsx b/site/src/components/docs/Selectors.tsx index 8e2e89df..6b6b38c7 100644 --- a/site/src/components/docs/Selectors.tsx +++ b/site/src/components/docs/Selectors.tsx @@ -35,10 +35,20 @@ export function Selectors({ currentFramework, currentSlug }: SelectorProps) { }); if (shouldReplace) { - // Maintaining the current slug, navigate without pushing onto the history stack + // Base UI's scroll lock transfers html.scrollTop → body.scrollTop + const scrollLocked = document.documentElement.hasAttribute('data-base-ui-scroll-locked'); + const scrollY = scrollLocked ? document.body.scrollTop : window.scrollY; + + try { + sessionStorage.setItem( + 'vjs-page-scroll', + JSON.stringify({ url: new URL(url, window.location.origin).pathname, scrollY }) + ); + } catch { + // Ignore storage errors + } window.location.replace(url); } else { - // Changing slug, use normal navigation window.location.href = url; } }; diff --git a/site/src/components/installation/JSPickerClient.tsx b/site/src/components/installation/JSPickerClient.tsx index 8b2f2e7f..da04fb3d 100644 --- a/site/src/components/installation/JSPickerClient.tsx +++ b/site/src/components/installation/JSPickerClient.tsx @@ -17,7 +17,6 @@ interface Props { } export default function JSPickerClient({ currentFramework, currentStyle, currentSlug }: Props) { - // TODO: use astro view transitions to preserve scroll position when switching from the same slug to the same slug const handleFrameworkChange = (newFramework: SupportedFramework | null) => { if (newFramework === null) return; if (!isValidFramework(newFramework)) return; @@ -30,10 +29,20 @@ export default function JSPickerClient({ currentFramework, currentStyle, current }); if (shouldReplace) { - // Maintaining the current slug, navigate without pushing onto the history stack + // Base UI's scroll lock transfers html.scrollTop → body.scrollTop + const scrollLocked = document.documentElement.hasAttribute('data-base-ui-scroll-locked'); + const scrollY = scrollLocked ? document.body.scrollTop : window.scrollY; + + try { + sessionStorage.setItem( + 'vjs-page-scroll', + JSON.stringify({ url: new URL(url, window.location.origin).pathname, scrollY }) + ); + } catch { + // Ignore storage errors + } window.location.replace(url); } else { - // Changing slug, use normal navigation window.location.href = url; } };