summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2024-09-07 00:18:54 +0200
committerGitHub2024-09-07 00:18:54 +0200
commit756e85e8e814657c42c4a6f9c299b5bef32aee22 (patch)
tree5407c8a20dc87d2031d4f507604d27baa102a97b
parentdee40c0c74b9a82625aa8b444910c56335325ad8 (diff)
downloadIT.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.md5
-rw-r--r--packages/starlight/components/Sidebar.astro38
-rw-r--r--packages/starlight/components/SidebarPersistState.ts6
-rw-r--r--packages/starlight/components/SidebarPersister.astro72
-rw-r--r--packages/starlight/components/SidebarRestorePoint.astro12
-rw-r--r--packages/starlight/components/SidebarSublist.astro2
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>