import * as React from 'react' import { LayoutChangeEvent, FlatList, ScrollView, StyleSheet, View, NativeScrollEvent, } from 'react-native' import Animated, { useAnimatedStyle, useSharedValue, runOnJS, runOnUI, scrollTo, useAnimatedRef, AnimatedRef, SharedValue, } from 'react-native-reanimated' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {TabBar} from './TabBar' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' const SCROLLED_DOWN_LIMIT = 200 export interface PagerWithHeaderChildParams { headerHeight: number isFocused: boolean onScroll: OnScrollHandler isScrolledDown: boolean scrollElRef: React.MutableRefObject | ScrollView | null> } export interface PagerWithHeaderProps { testID?: string children: | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[] | ((props: PagerWithHeaderChildParams) => JSX.Element) items: string[] isHeaderReady: boolean renderHeader?: () => JSX.Element initialPage?: number onPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void } export const PagerWithHeader = React.forwardRef( function PageWithHeaderImpl( { children, testID, items, isHeaderReady, renderHeader, initialPage, onPageSelected, onCurrentPageSelected, }: PagerWithHeaderProps, ref, ) { const [currentPage, setCurrentPage] = React.useState(0) const [tabBarHeight, setTabBarHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) const [isScrolledDown, setIsScrolledDown] = React.useState(false) const scrollY = useSharedValue(0) const headerHeight = headerOnlyHeight + tabBarHeight // capture the header bar sizing const onTabBarLayout = React.useCallback( (evt: LayoutChangeEvent) => { setTabBarHeight(evt.nativeEvent.layout.height) }, [setTabBarHeight], ) const onHeaderOnlyLayout = React.useCallback( (evt: LayoutChangeEvent) => { setHeaderOnlyHeight(evt.nativeEvent.layout.height) }, [setHeaderOnlyHeight], ) const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { return ( ) }, [ headerOnlyHeight, items, isHeaderReady, renderHeader, currentPage, onCurrentPageSelected, onTabBarLayout, onHeaderOnlyLayout, scrollY, testID, ], ) const scrollRefs = useSharedValue[]>([]) const registerRef = (scrollRef: AnimatedRef, index: number) => { scrollRefs.modify(refs => { 'worklet' refs[index] = scrollRef return refs }) } const lastForcedScrollY = useSharedValue(0) const adjustScrollForOtherPages = () => { 'worklet' const currentScrollY = scrollY.value const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) if (lastForcedScrollY.value !== forcedScrollY) { lastForcedScrollY.value = forcedScrollY const refs = scrollRefs.value for (let i = 0; i < refs.length; i++) { if (i !== currentPage) { // This needs to run on the UI thread. scrollTo(refs[i], 0, forcedScrollY, false) } } } } const throttleTimeout = React.useRef | null>( null, ) const queueThrottledOnScroll = useNonReactiveCallback(() => { if (!throttleTimeout.current) { throttleTimeout.current = setTimeout(() => { throttleTimeout.current = null runOnUI(adjustScrollForOtherPages)() const nextIsScrolledDown = scrollY.value > SCROLLED_DOWN_LIMIT if (isScrolledDown !== nextIsScrolledDown) { React.startTransition(() => { setIsScrolledDown(nextIsScrolledDown) }) } }, 80 /* Sync often enough you're unlikely to catch it unsynced */) } }) const onScrollWorklet = React.useCallback( (e: NativeScrollEvent) => { 'worklet' const nextScrollY = e.contentOffset.y scrollY.value = nextScrollY runOnJS(queueThrottledOnScroll)() }, [scrollY, queueThrottledOnScroll], ) const onPageSelectedInner = React.useCallback( (index: number) => { setCurrentPage(index) onPageSelected?.(index) }, [onPageSelected, setCurrentPage], ) const onPageSelecting = React.useCallback((index: number) => { setCurrentPage(index) }, []) return ( {toArray(children) .filter(Boolean) .map((child, i) => { const isReady = isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 return ( ) => registerRef(r, i)} renderTab={child} /> ) })} ) }, ) let PagerTabBar = ({ currentPage, headerOnlyHeight, isHeaderReady, items, scrollY, testID, renderHeader, onHeaderOnlyLayout, onTabBarLayout, onCurrentPageSelected, onSelect, }: { currentPage: number headerOnlyHeight: number isHeaderReady: boolean items: string[] testID?: string scrollY: SharedValue renderHeader?: () => JSX.Element onHeaderOnlyLayout: (e: LayoutChangeEvent) => void onTabBarLayout: (e: LayoutChangeEvent) => void onCurrentPageSelected?: (index: number) => void onSelect?: (index: number) => void }): React.ReactNode => { const {isMobile} = useWebMediaQueries() const headerTransform = useAnimatedStyle(() => ({ transform: [ { translateY: Math.min(Math.min(scrollY.value, headerOnlyHeight) * -1, 0), }, ], })) return ( {renderHeader?.()} ) } PagerTabBar = React.memo(PagerTabBar) function PagerItem({ headerHeight, isReady, isFocused, isScrolledDown, onScrollWorklet, renderTab, registerRef, }: { headerHeight: number isFocused: boolean isReady: boolean isScrolledDown: boolean registerRef: (scrollRef: AnimatedRef) => void onScrollWorklet: (e: NativeScrollEvent) => void renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null }) { const scrollElRef = useAnimatedRef() registerRef(scrollElRef) const scrollHandler = React.useMemo( () => ({onScroll: onScrollWorklet}), [onScrollWorklet], ) if (!isReady || renderTab == null) { return null } return renderTab({ headerHeight, isFocused, isScrolledDown, onScroll: scrollHandler, scrollElRef: scrollElRef as React.MutableRefObject< FlatList | ScrollView | null >, }) } const styles = StyleSheet.create({ tabBarMobile: { position: 'absolute', zIndex: 1, top: 0, left: 0, width: '100%', }, tabBarDesktop: { position: 'absolute', zIndex: 1, top: 0, // @ts-ignore Web only -prf left: 'calc(50% - 299px)', width: 598, }, }) function noop() { 'worklet' } function toArray(v: T | T[]): T[] { if (Array.isArray(v)) { return v } return [v] }