an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

reusable tabs

rimar1337 9d9b2b83 13552d53

Changed files
+128 -2
src
components
utils
+124
src/components/ReusableTabRoute.tsx
··· 1 + import * as TabsPrimitive from "@radix-ui/react-tabs"; 2 + import { useAtom } from "jotai"; 3 + import { useEffect, useLayoutEffect } from "react"; 4 + 5 + import { isAtTopAtom, reusableTabRouteScrollAtom } from "~/utils/atoms"; 6 + 7 + /** 8 + * Please wrap your Route in a div, do not return a top-level fragment, 9 + * it will break navigation scroll restoration 10 + */ 11 + export function ReusableTabRoute({ 12 + route, 13 + tabs, 14 + }: { 15 + route: string; 16 + tabs: Record<string, React.ReactNode>; 17 + }) { 18 + const [reusableTabState, setReusableTabState] = useAtom( 19 + reusableTabRouteScrollAtom 20 + ); 21 + const [isAtTop] = useAtom(isAtTopAtom); 22 + 23 + const routeState = reusableTabState?.[route] ?? { 24 + activeTab: Object.keys(tabs)[0], 25 + scrollPositions: {}, 26 + }; 27 + const activeTab = routeState.activeTab; 28 + 29 + const handleValueChange = (newTab: string) => { 30 + setReusableTabState((prev) => { 31 + const current = prev?.[route] ?? routeState; 32 + return { 33 + ...prev, 34 + [route]: { 35 + ...current, 36 + scrollPositions: { 37 + ...current.scrollPositions, 38 + [current.activeTab]: window.scrollY, 39 + }, 40 + activeTab: newTab, 41 + }, 42 + }; 43 + }); 44 + }; 45 + 46 + // // todo, warning experimental, usually this doesnt work, 47 + // // like at all, and i usually do this for each tab 48 + // useLayoutEffect(() => { 49 + // const savedScroll = routeState.scrollPositions[activeTab] ?? 0; 50 + // window.scrollTo({ top: savedScroll }); 51 + // // eslint-disable-next-line react-hooks/exhaustive-deps 52 + // }, [activeTab, route]); 53 + 54 + useLayoutEffect(() => { 55 + return () => { 56 + setReusableTabState((prev) => { 57 + const current = prev?.[route] ?? routeState; 58 + return { 59 + ...prev, 60 + [route]: { 61 + ...current, 62 + scrollPositions: { 63 + ...current.scrollPositions, 64 + [current.activeTab]: window.scrollY, 65 + }, 66 + }, 67 + }; 68 + }); 69 + }; 70 + // eslint-disable-next-line react-hooks/exhaustive-deps 71 + }, []); 72 + 73 + return ( 74 + <TabsPrimitive.Root 75 + value={activeTab} 76 + onValueChange={handleValueChange} 77 + className={`w-full`} 78 + > 79 + <TabsPrimitive.List 80 + className={`flex sticky top-[52px] bg-[var(--header-bg-light)] dark:bg-[var(--header-bg-dark)] z-[9] border-0 sm:border-b ${!isAtTop && "shadow-sm"} sm:shadow-none sm:dark:bg-gray-950 sm:bg-white border-gray-200 dark:border-gray-700`} 81 + > 82 + {Object.entries(tabs).map(([key]) => ( 83 + <TabsPrimitive.Trigger key={key} value={key} className="m3tab"> 84 + {key} 85 + </TabsPrimitive.Trigger> 86 + ))} 87 + </TabsPrimitive.List> 88 + 89 + {Object.entries(tabs).map(([key, node]) => ( 90 + <TabsPrimitive.Content key={key} value={key} className="flex-1 min-h-[80dvh]"> 91 + {activeTab === key && node} 92 + </TabsPrimitive.Content> 93 + ))} 94 + </TabsPrimitive.Root> 95 + ); 96 + } 97 + 98 + export function useReusableTabScrollRestore(route: string) { 99 + const [reusableTabState] = useAtom( 100 + reusableTabRouteScrollAtom 101 + ); 102 + 103 + const routeState = reusableTabState?.[route]; 104 + const activeTab = routeState?.activeTab; 105 + 106 + useEffect(() => { 107 + const savedScroll = activeTab ? routeState?.scrollPositions[activeTab] ?? 0 : 0; 108 + //window.scrollTo(0, savedScroll); 109 + window.scrollTo({ top: savedScroll }); 110 + // eslint-disable-next-line react-hooks/exhaustive-deps 111 + }, []); 112 + } 113 + 114 + 115 + /* 116 + 117 + const [notifState] = useAtom(notificationsScrollAtom); 118 + const activeTab = notifState.activeTab; 119 + useEffect(() => { 120 + const savedY = notifState.scrollPositions[activeTab] ?? 0; 121 + window.scrollTo(0, savedY); 122 + }, [activeTab, notifState.scrollPositions]); 123 + 124 + */
+4 -2
src/utils/atoms.ts
··· 21 21 {} 22 22 ); 23 23 24 - type NotificationsScrollState = { 24 + type TabRouteScrollState = { 25 25 activeTab: string; 26 26 scrollPositions: Record<string, number>; 27 27 }; 28 - export const notificationsScrollAtom = atom<NotificationsScrollState>({ 28 + export const notificationsScrollAtom = atom<TabRouteScrollState>({ 29 29 activeTab: "mentions", 30 30 scrollPositions: {}, 31 31 }); 32 + 33 + export const reusableTabRouteScrollAtom = atom<Record<string, TabRouteScrollState | undefined> | undefined>({}); 32 34 33 35 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 34 36 "likedPosts",