diff options
author | Chris Swithinbank | 2024-09-07 00:18:54 +0200 |
---|---|---|
committer | GitHub | 2024-09-07 00:18:54 +0200 |
commit | 756e85e8e814657c42c4a6f9c299b5bef32aee22 (patch) | |
tree | 5407c8a20dc87d2031d4f507604d27baa102a97b | |
parent | dee40c0c74b9a82625aa8b444910c56335325ad8 (diff) | |
download | IT.starlight-756e85e8e814657c42c4a6f9c299b5bef32aee22.tar.gz IT.starlight-756e85e8e814657c42c4a6f9c299b5bef32aee22.tar.bz2 IT.starlight-756e85e8e814657c42c4a6f9c299b5bef32aee22.zip |
Refactor sidebar persistence logic for better slow device performance (#2242)
Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
-rw-r--r-- | .changeset/heavy-forks-help.md | 5 | ||||
-rw-r--r-- | packages/starlight/components/Sidebar.astro | 38 | ||||
-rw-r--r-- | packages/starlight/components/SidebarPersistState.ts | 6 | ||||
-rw-r--r-- | packages/starlight/components/SidebarPersister.astro | 72 | ||||
-rw-r--r-- | packages/starlight/components/SidebarRestorePoint.astro | 12 | ||||
-rw-r--r-- | packages/starlight/components/SidebarSublist.astro | 2 |
6 files changed, 98 insertions, 37 deletions
diff --git a/.changeset/heavy-forks-help.md b/.changeset/heavy-forks-help.md new file mode 100644 index 00000000..17f017ff --- /dev/null +++ b/.changeset/heavy-forks-help.md @@ -0,0 +1,5 @@ +--- +"@astrojs/starlight": patch +--- + +Refactors the logic for persisting and restoring sidebar state across navigations for better performance on slow or busy devices diff --git a/packages/starlight/components/Sidebar.astro b/packages/starlight/components/Sidebar.astro index c47d0315..1dd4c047 100644 --- a/packages/starlight/components/Sidebar.astro +++ b/packages/starlight/components/Sidebar.astro @@ -2,46 +2,16 @@ import type { Props } from '../props'; import MobileMenuFooter from 'virtual:starlight/components/MobileMenuFooter'; -import { getSidebarHash } from '../utils/navigation'; +import SidebarPersister from './SidebarPersister.astro'; import SidebarSublist from './SidebarSublist.astro'; const { sidebar } = Astro.props; -const hash = getSidebarHash(sidebar); --- -<sl-sidebar-state-persist data-hash={hash}> +<SidebarPersister {...Astro.props}> <SidebarSublist sublist={sidebar} /> -</sl-sidebar-state-persist> +</SidebarPersister> + <div class="md:sl-hidden"> <MobileMenuFooter {...Astro.props} /> </div> - -{ - /* - Inline script to restore sidebar state as soon as possible. - - On smaller viewports, restoring state is skipped as the sidebar is collapsed inside a menu. - - The state is parsed from session storage and restored. - - This is a progressive enhancement, so any errors are swallowed silently. - */ -} -<script is:inline> - (() => { - try { - if (!matchMedia('(min-width: 50em)').matches) return; - const scroller = document.getElementById('starlight__sidebar'); - /** @type {HTMLElement | null} */ - const target = document.querySelector('sl-sidebar-state-persist'); - const state = JSON.parse(sessionStorage.getItem('sl-sidebar-state') || '0'); - if (!scroller || !target || !state || target.dataset.hash !== state.hash) return; - target - .querySelectorAll('details') - .forEach((el, idx) => typeof state.open[idx] === 'boolean' && (el.open = state.open[idx])); - scroller.scrollTop = state.scroll; - } catch {} - })(); -</script> -<style> - sl-sidebar-state-persist { - display: contents; - } -</style> diff --git a/packages/starlight/components/SidebarPersistState.ts b/packages/starlight/components/SidebarPersistState.ts index 029dd95c..4c4513ef 100644 --- a/packages/starlight/components/SidebarPersistState.ts +++ b/packages/starlight/components/SidebarPersistState.ts @@ -1,7 +1,6 @@ // Collect required elements from the DOM. const scroller = document.getElementById('starlight__sidebar'); const target = scroller?.querySelector<HTMLElement>('sl-sidebar-state-persist'); -const details = [...(target?.querySelectorAll('details') || [])]; /** Starlight uses this key to store sidebar state in `sessionStorage`. */ const storageKey = 'sl-sidebar-state'; @@ -58,8 +57,9 @@ target?.addEventListener('click', (event) => { // This excludes clicks outside of the `<summary>`, which don’t trigger toggles. const toggledDetails = event.target.closest('summary')?.closest('details'); if (!toggledDetails) return; - const index = details.indexOf(toggledDetails); - if (index === -1) return; + const restoreElement = toggledDetails.querySelector<HTMLElement>('sl-sidebar-restore'); + const index = parseInt(restoreElement?.dataset.index || ''); + if (isNaN(index)) return; setToggleState(!toggledDetails.open, index); }); diff --git a/packages/starlight/components/SidebarPersister.astro b/packages/starlight/components/SidebarPersister.astro new file mode 100644 index 00000000..ae485c97 --- /dev/null +++ b/packages/starlight/components/SidebarPersister.astro @@ -0,0 +1,72 @@ +--- +/* + This component is designed to wrap the tree of `<SidebarSublist>` components in the sidebar. + + It does the following: + - Wraps the tree in an `<sl-sidebar-state-persist>` custom element + - Before the tree renders, adds an inline script which loads state and defines + the behaviour for the `<sl-sidebar-restore>` custom element. + - After the tree renders, adds an inline script which restores the sidebar scroll state. + + Notes: + - On smaller viewports, restoring state is skipped as the sidebar is collapsed inside a menu. + - The state is parsed from session storage and restored. + - This is a progressive enhancement, so any errors are swallowed silently. +*/ + +import type { Props } from '../props'; +import { getSidebarHash } from '../utils/navigation'; + +const hash = getSidebarHash(Astro.props.sidebar); + +declare global { + interface Window { + /** Restored scroll position. Briefly stored on the `window` global to pass between inline scripts. */ + _starlightScrollRestore?: number; + } +} +--- + +<sl-sidebar-state-persist data-hash={hash}> + <script is:inline> + (() => { + try { + if (!matchMedia('(min-width: 50em)').matches) return; + /** @type {HTMLElement | null} */ + const target = document.querySelector('sl-sidebar-state-persist'); + const state = JSON.parse(sessionStorage.getItem('sl-sidebar-state') || '0'); + if (!target || !state || target.dataset.hash !== state.hash) return; + window._starlightScrollRestore = state.scroll; + customElements.define( + 'sl-sidebar-restore', + class SidebarRestore extends HTMLElement { + connectedCallback() { + try { + const idx = parseInt(this.dataset.index || ''); + const details = this.closest('details'); + if (details && typeof state.open[idx] === 'boolean') details.open = state.open[idx]; + } catch {} + } + } + ); + } catch {} + })(); + </script> + + <slot /> + + <script is:inline> + (() => { + const scroller = document.getElementById('starlight__sidebar'); + if (!window._starlightScrollRestore || !scroller) return; + scroller.scrollTop = window._starlightScrollRestore; + delete window._starlightScrollRestore; + })(); + </script> +</sl-sidebar-state-persist> + +<style> + sl-sidebar-state-persist { + display: contents; + } +</style> diff --git a/packages/starlight/components/SidebarRestorePoint.astro b/packages/starlight/components/SidebarRestorePoint.astro new file mode 100644 index 00000000..d3d96939 --- /dev/null +++ b/packages/starlight/components/SidebarRestorePoint.astro @@ -0,0 +1,12 @@ +--- +/** Unique symbol for storing a running index in `locals`. */ +const currentGroupIndexSymbol = Symbol.for('starlight-sidebar-group-index'); +const locals = Astro.locals as Record<typeof currentGroupIndexSymbol, number>; + +/** The current sidebar group’s index retrieved from `locals` if set, starting at `0`. */ +const index = locals[currentGroupIndexSymbol] || 0; +// Increment the index for the next instance. +locals[currentGroupIndexSymbol] = index + 1; +--- + +<sl-sidebar-restore data-index={index}></sl-sidebar-restore> diff --git a/packages/starlight/components/SidebarSublist.astro b/packages/starlight/components/SidebarSublist.astro index 23252501..b521ba13 100644 --- a/packages/starlight/components/SidebarSublist.astro +++ b/packages/starlight/components/SidebarSublist.astro @@ -2,6 +2,7 @@ import { flattenSidebar, type SidebarEntry } from '../utils/navigation'; import Icon from '../user-components/Icon.astro'; import Badge from '../user-components/Badge.astro'; +import SidebarRestorePoint from './SidebarRestorePoint.astro'; interface Props { sublist: SidebarEntry[]; @@ -35,6 +36,7 @@ const { sublist, nested } = Astro.props; <details open={flattenSidebar(entry.entries).some((i) => i.isCurrent) || !entry.collapsed} > + <SidebarRestorePoint /> <summary> <div class="group-label"> <span class="large">{entry.label}</span> |