forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {
2 Children,
3 type JSX,
4 useCallback,
5 useImperativeHandle,
6 useRef,
7 useState,
8} from 'react'
9import {View} from 'react-native'
10import {flushSync} from 'react-dom'
11
12import {s} from '#/lib/styles'
13import {atoms as a} from '#/alf'
14
15export interface PagerRef {
16 setPage: (index: number) => void
17}
18
19export interface RenderTabBarFnProps {
20 selectedPage: number
21 onSelect?: (index: number) => void
22 tabBarAnchor?: JSX.Element
23}
24export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
25
26interface Props {
27 ref?: React.Ref<PagerRef>
28 initialPage?: number
29 renderTabBar: RenderTabBarFn
30 onPageSelected?: (index: number) => void
31}
32
33export function Pager({
34 ref,
35 children,
36 initialPage = 0,
37 renderTabBar,
38 onPageSelected,
39}: React.PropsWithChildren<Props>) {
40 const [selectedPage, setSelectedPage] = useState(initialPage)
41 const scrollYs = useRef<Array<number | null>>([])
42 const anchorRef = useRef(null)
43
44 useImperativeHandle(ref, () => ({
45 setPage: (index: number) => {
46 onTabBarSelect(index)
47 },
48 }))
49
50 const onTabBarSelect = useCallback(
51 (index: number) => {
52 const scrollY = window.scrollY
53 // We want to determine if the tabbar is already "sticking" at the top (in which
54 // case we should preserve and restore scroll), or if it is somewhere below in the
55 // viewport (in which case a scroll jump would be jarring). We determine this by
56 // measuring where the "anchor" element is (which we place just above the tabbar).
57 let anchorTop = anchorRef.current
58 ? (anchorRef.current as Element).getBoundingClientRect().top
59 : -scrollY // If there's no anchor, treat the top of the page as one.
60 const isSticking = anchorTop <= 5 // This would be 0 if browser scrollTo() was reliable.
61
62 if (isSticking) {
63 scrollYs.current[selectedPage] = window.scrollY
64 } else {
65 scrollYs.current[selectedPage] = null
66 }
67 flushSync(() => {
68 setSelectedPage(index)
69 onPageSelected?.(index)
70 })
71 if (isSticking) {
72 const restoredScrollY = scrollYs.current[index]
73 if (restoredScrollY != null) {
74 window.scrollTo(0, restoredScrollY)
75 } else {
76 window.scrollTo(0, scrollY + anchorTop)
77 }
78 }
79 },
80 [selectedPage, setSelectedPage, onPageSelected],
81 )
82
83 return (
84 <View style={s.hContentRegion}>
85 {renderTabBar({
86 selectedPage,
87 tabBarAnchor: <View ref={anchorRef} />,
88 onSelect: e => onTabBarSelect(e),
89 })}
90 {Children.map(children, (child, i) => (
91 <View
92 style={selectedPage === i ? a.flex_1 : a.hidden}
93 key={`page-${i}`}>
94 {child}
95 </View>
96 ))}
97 </View>
98 )
99}