diff --git a/src/components/ReusableTabRoute.tsx b/src/components/ReusableTabRoute.tsx new file mode 100644 index 0000000..f8ab464 --- /dev/null +++ b/src/components/ReusableTabRoute.tsx @@ -0,0 +1,124 @@ +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { useAtom } from "jotai"; +import { useEffect, useLayoutEffect } from "react"; + +import { isAtTopAtom, reusableTabRouteScrollAtom } from "~/utils/atoms"; + +/** + * Please wrap your Route in a div, do not return a top-level fragment, + * it will break navigation scroll restoration + */ +export function ReusableTabRoute({ + route, + tabs, +}: { + route: string; + tabs: Record; +}) { + const [reusableTabState, setReusableTabState] = useAtom( + reusableTabRouteScrollAtom + ); + const [isAtTop] = useAtom(isAtTopAtom); + + const routeState = reusableTabState?.[route] ?? { + activeTab: Object.keys(tabs)[0], + scrollPositions: {}, + }; + const activeTab = routeState.activeTab; + + const handleValueChange = (newTab: string) => { + setReusableTabState((prev) => { + const current = prev?.[route] ?? routeState; + return { + ...prev, + [route]: { + ...current, + scrollPositions: { + ...current.scrollPositions, + [current.activeTab]: window.scrollY, + }, + activeTab: newTab, + }, + }; + }); + }; + + // // todo, warning experimental, usually this doesnt work, + // // like at all, and i usually do this for each tab + // useLayoutEffect(() => { + // const savedScroll = routeState.scrollPositions[activeTab] ?? 0; + // window.scrollTo({ top: savedScroll }); + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [activeTab, route]); + + useLayoutEffect(() => { + return () => { + setReusableTabState((prev) => { + const current = prev?.[route] ?? routeState; + return { + ...prev, + [route]: { + ...current, + scrollPositions: { + ...current.scrollPositions, + [current.activeTab]: window.scrollY, + }, + }, + }; + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + + {Object.entries(tabs).map(([key]) => ( + + {key} + + ))} + + + {Object.entries(tabs).map(([key, node]) => ( + + {activeTab === key && node} + + ))} + + ); +} + +export function useReusableTabScrollRestore(route: string) { + const [reusableTabState] = useAtom( + reusableTabRouteScrollAtom + ); + + const routeState = reusableTabState?.[route]; + const activeTab = routeState?.activeTab; + + useEffect(() => { + const savedScroll = activeTab ? routeState?.scrollPositions[activeTab] ?? 0 : 0; + //window.scrollTo(0, savedScroll); + window.scrollTo({ top: savedScroll }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +} + + +/* + + const [notifState] = useAtom(notificationsScrollAtom); + const activeTab = notifState.activeTab; + useEffect(() => { + const savedY = notifState.scrollPositions[activeTab] ?? 0; + window.scrollTo(0, savedY); + }, [activeTab, notifState.scrollPositions]); + + */ \ No newline at end of file diff --git a/src/utils/atoms.ts b/src/utils/atoms.ts index e5e5c4f..92ae607 100644 --- a/src/utils/atoms.ts +++ b/src/utils/atoms.ts @@ -21,15 +21,17 @@ export const feedScrollPositionsAtom = atomWithStorage>( {} ); -type NotificationsScrollState = { +type TabRouteScrollState = { activeTab: string; scrollPositions: Record; }; -export const notificationsScrollAtom = atom({ +export const notificationsScrollAtom = atom({ activeTab: "mentions", scrollPositions: {}, }); +export const reusableTabRouteScrollAtom = atom | undefined>({}); + export const likedPostsAtom = atomWithStorage>( "likedPosts", {}