Skip to content

Commit 316c8d8

Browse files
fix(web): maintain sidebar scroll position when navigating between chats
Generated with [Linear](https://linear.app/sourcebot/issue/SOU-1483/maintain-sidebar-scroll-position-when-opening-a-e6d7#agent-session-05e85428) Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com>
1 parent e706330 commit 316c8d8

1 file changed

Lines changed: 21 additions & 2 deletions

File tree

packages/web/src/app/(app)/@sidebar/components/sidebarBase.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,18 @@ import { signOut } from "next-auth/react";
4040
import { useTheme } from "next-themes";
4141
import Link from "next/link";
4242
import posthog from "posthog-js";
43-
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
43+
import { ReactNode, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4444
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
4545
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
4646
import { Separator } from "@/components/ui/separator";
4747
import { WhatsNewSidebarButton } from "./whatsNewSidebarButton";
4848
import { UpgradeButton } from "./upgradeButton";
4949
import { useIsMobile } from "@/hooks/use-mobile";
5050

51+
// Retains the sidebar's scroll position across route-driven remounts of the
52+
// `@sidebar` slot. Module-level so it survives remounts but resets on reload.
53+
let lastScrollTop = 0;
54+
5155
interface SidebarBaseProps {
5256
session: Session | null;
5357
collapsible?: "icon" | "offcanvas" | "none";
@@ -61,12 +65,27 @@ export function SidebarBase({ session, collapsible = "icon", headerContent, chil
6165
const contentRef = useRef<HTMLDivElement>(null);
6266
const isMobile = useIsMobile();
6367

68+
// The sidebar lives in the `@sidebar` parallel route slot, whose catch-all
69+
// segment remounts on navigation between chats. Persist the scroll position
70+
// across those remounts so it isn't reset to the top each time.
71+
useLayoutEffect(() => {
72+
const el = contentRef.current;
73+
if (!el) {
74+
return;
75+
}
76+
el.scrollTop = lastScrollTop;
77+
setIsScrolled(el.scrollTop > 0);
78+
}, []);
79+
6480
useEffect(() => {
6581
const el = contentRef.current;
6682
if (!el) {
6783
return;
6884
}
69-
const handleScroll = () => setIsScrolled(el.scrollTop > 0);
85+
const handleScroll = () => {
86+
lastScrollTop = el.scrollTop;
87+
setIsScrolled(el.scrollTop > 0);
88+
};
7089
el.addEventListener("scroll", handleScroll);
7190
return () => el.removeEventListener("scroll", handleScroll);
7291
}, []);

0 commit comments

Comments
 (0)