From 32e77e0291e776f8f61e9f2750387241696912bb Mon Sep 17 00:00:00 2001 From: kOaDT Date: Tue, 10 Mar 2026 22:59:22 +0100 Subject: [PATCH] fix: preserve board scroll position when navigating back from a card (#431) --- apps/web/src/hooks/useScrollRestore.ts | 44 ++++++++++++++++++++++++++ apps/web/src/views/board/index.tsx | 3 ++ 2 files changed, 47 insertions(+) create mode 100644 apps/web/src/hooks/useScrollRestore.ts diff --git a/apps/web/src/hooks/useScrollRestore.ts b/apps/web/src/hooks/useScrollRestore.ts new file mode 100644 index 00000000..b5e5078e --- /dev/null +++ b/apps/web/src/hooks/useScrollRestore.ts @@ -0,0 +1,44 @@ +import type { RefObject } from "react"; +import type { NextRouter } from "next/router"; +import { useEffect, useRef } from "react"; + +const scrollPositions = new Map(); + +export function useScrollRestore( + boardId: string | null | undefined, + scrollRef: RefObject, + router: NextRouter, + isReady: boolean, +) { + const restored = useRef(false); + + useEffect(() => { + if (!boardId) return; + + restored.current = false; + + const saveScrollPosition = () => { + if (scrollRef.current) { + scrollPositions.set(boardId, scrollRef.current.scrollLeft); + } + }; + + router.events.on("routeChangeStart", saveScrollPosition); + return () => router.events.off("routeChangeStart", saveScrollPosition); + }, [boardId, router.events, scrollRef]); + + useEffect(() => { + if (restored.current || !isReady || !boardId) return; + restored.current = true; + + const saved = scrollPositions.get(boardId); + if (saved === undefined) return; + + // StrictModeDroppable delays rendering by one requestAnimationFrame + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (scrollRef.current) scrollRef.current.scrollLeft = saved; + }); + }); + }, [isReady, scrollRef, boardId]); +} diff --git a/apps/web/src/views/board/index.tsx b/apps/web/src/views/board/index.tsx index 647c1d3f..6074a56e 100644 --- a/apps/web/src/views/board/index.tsx +++ b/apps/web/src/views/board/index.tsx @@ -26,6 +26,7 @@ import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppab import { Tooltip } from "~/components/Tooltip"; import { EditYouTubeModal } from "~/components/YouTubeEmbed/EditYouTubeModal"; import { useDragToScroll } from "~/hooks/useDragToScroll"; +import { useScrollRestore } from "~/hooks/useScrollRestore"; import { usePermissions } from "~/hooks/usePermissions"; import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts"; import { useModal } from "~/providers/modal"; @@ -141,6 +142,8 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) { const isLoading = isInitialLoading || isQueryLoading; + useScrollRestore(boardId, scrollRef, router, !isLoading && (boardData?.lists.length ?? 0) > 0); + const updateListMutation = api.list.update.useMutation({ onMutate: async (args) => { await utils.board.byId.cancel();