an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
at main 3.6 kB view raw
1import * as TabsPrimitive from "@radix-ui/react-tabs"; 2import { useAtom } from "jotai"; 3import { useEffect, useLayoutEffect } from "react"; 4 5import { 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 */ 11export 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 98export 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 */