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