forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsidebar-scroll-preserver.tsx
More file actions
67 lines (60 loc) · 2.43 KB
/
Copy pathsidebar-scroll-preserver.tsx
File metadata and controls
67 lines (60 loc) · 2.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// SidebarScrollPreserver — restores the sidebar's scroll position
// across navigations.
//
// The sidebar is rendered per-page inside `ShellDocsLayout`, so each
// click on a sidebar link unmounts and remounts the whole tree. Without
// intervention the Radix ScrollAreaViewport snaps back to 0, which
// makes every navigation in a long sidebar (scroll down, click a
// section deep in the list) jarring — the item you just clicked
// disappears from view.
//
// This component:
// 1. Reads the saved scrollTop from sessionStorage on layout-effect
// (runs synchronously before paint, so there's no flicker).
// 2. Applies it to the sidebar's `[data-radix-scroll-area-viewport]`.
// 3. Registers a passive scroll listener that writes the latest
// scrollTop back to sessionStorage on every frame.
//
// Why sessionStorage: persists for the tab session (long enough that
// reload preserves position) but isolates per-tab so two tabs don't
// fight over a shared key.
//
// Mounted once, inside `ShellDocsLayout`. The component renders nothing
// itself — it just attaches the listener.
"use client";
import { useLayoutEffect } from "react";
const STORAGE_KEY = "shell-docs:sidebar-scroll-top";
export function SidebarScrollPreserver() {
useLayoutEffect(() => {
const viewport = document.querySelector<HTMLElement>(
"aside.shell-docs-sidebar [data-radix-scroll-area-viewport]",
);
if (!viewport) return;
// Restore saved scroll position. Doing this in useLayoutEffect
// means the assignment happens BEFORE the browser paints, so the
// user never sees the sidebar at 0 first.
const saved = sessionStorage.getItem(STORAGE_KEY);
if (saved !== null) {
const top = Number.parseInt(saved, 10);
if (Number.isFinite(top) && top > 0) {
viewport.scrollTop = top;
}
}
// Persist scroll on every change. `requestAnimationFrame`-coalesce
// so a continuous drag/scroll-wheel doesn't pound sessionStorage.
let rafId: number | null = null;
const onScroll = () => {
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
sessionStorage.setItem(STORAGE_KEY, String(viewport.scrollTop));
rafId = null;
});
};
viewport.addEventListener("scroll", onScroll, { passive: true });
return () => {
viewport.removeEventListener("scroll", onScroll);
if (rafId !== null) cancelAnimationFrame(rafId);
};
}, []);
return null;
}