Skip to content

Commit

Permalink
Refactor sidebar persistence logic for better slow device performance (
Browse files Browse the repository at this point in the history
…#2242)

Co-authored-by: HiDeoo <494699+HiDeoo@users.noreply.github.com>
  • Loading branch information
delucis and HiDeoo committed Sep 6, 2024
1 parent dee40c0 commit 756e85e
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/heavy-forks-help.md
Original file line number Diff line number Diff line change
@@ -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
38 changes: 4 additions & 34 deletions packages/starlight/components/Sidebar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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>
6 changes: 3 additions & 3 deletions packages/starlight/components/SidebarPersistState.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});

Expand Down
72 changes: 72 additions & 0 deletions packages/starlight/components/SidebarPersister.astro
Original file line number Diff line number Diff line change
@@ -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>
12 changes: 12 additions & 0 deletions packages/starlight/components/SidebarRestorePoint.astro
Original file line number Diff line number Diff line change
@@ -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>
2 changes: 2 additions & 0 deletions packages/starlight/components/SidebarSublist.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit 756e85e

Please sign in to comment.