Files
bsky-social-app/src/view/com/pager/Pager.web.tsx
T
dan 5a313c2d10 [Nicer Tabs] Fork TabBar, simplify Pager (#6762)
* Fork TabBar.web.tsx

* Trim dead code from both forks

* Remove onPageSelecting event

It's difficult to tell what exactly it's supposed to represent, and in practice it's not really used aside from logging. Let's rip it out for now to keep other changes simpler.

* Remove early onPageSelected call

It was added to try to do some work eagerly when we're sure which way the scroll is snapping. This is not necessarily a good idea though. It schedules a potentially expensive re-render right during the deceleration animation, which is not great. Whatever we're optimizing there, we should optimize smarter (e.g. prewarm just the network call). The other thing it used to help with is triggering the pager header autoscroll earlier. But we're going to rewrite that part differently anyway so that's not relevant either.

* Prune more dead code from the native version

We'll have to revisit this when adding tablet support but for now I'd prefer to remove a codepath that is not being tested or ever run.

* Use regular ScrollView on native

The Draggable thing was needed for web-only behavior so we can drop it in the native fork.
2024-12-03 01:12:58 +00:00

86 lines
2.5 KiB
TypeScript

import React from 'react'
import {View} from 'react-native'
import {flushSync} from 'react-dom'
import {s} from '#/lib/styles'
export interface RenderTabBarFnProps {
selectedPage: number
onSelect?: (index: number) => void
tabBarAnchor?: JSX.Element
}
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
interface Props {
initialPage?: number
renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void
}
export const Pager = React.forwardRef(function PagerImpl(
{
children,
initialPage = 0,
renderTabBar,
onPageSelected,
}: React.PropsWithChildren<Props>,
ref,
) {
const [selectedPage, setSelectedPage] = React.useState(initialPage)
const scrollYs = React.useRef<Array<number | null>>([])
const anchorRef = React.useRef(null)
React.useImperativeHandle(ref, () => ({
setPage: (index: number) => {
onTabBarSelect(index)
},
}))
const onTabBarSelect = React.useCallback(
(index: number) => {
const scrollY = window.scrollY
// We want to determine if the tabbar is already "sticking" at the top (in which
// case we should preserve and restore scroll), or if it is somewhere below in the
// viewport (in which case a scroll jump would be jarring). We determine this by
// measuring where the "anchor" element is (which we place just above the tabbar).
let anchorTop = anchorRef.current
? (anchorRef.current as Element).getBoundingClientRect().top
: -scrollY // If there's no anchor, treat the top of the page as one.
const isSticking = anchorTop <= 5 // This would be 0 if browser scrollTo() was reliable.
if (isSticking) {
scrollYs.current[selectedPage] = window.scrollY
} else {
scrollYs.current[selectedPage] = null
}
flushSync(() => {
setSelectedPage(index)
onPageSelected?.(index)
})
if (isSticking) {
const restoredScrollY = scrollYs.current[index]
if (restoredScrollY != null) {
window.scrollTo(0, restoredScrollY)
} else {
window.scrollTo(0, scrollY + anchorTop)
}
}
},
[selectedPage, setSelectedPage, onPageSelected],
)
return (
<View style={s.hContentRegion}>
{renderTabBar({
selectedPage,
tabBarAnchor: <View ref={anchorRef} />,
onSelect: e => onTabBarSelect(e),
})}
{React.Children.map(children, (child, i) => (
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
{child}
</View>
))}
</View>
)
})