import * as React from 'react' import { LayoutChangeEvent, NativeScrollEvent, StyleSheet, View, } from 'react-native' import Animated, { Easing, useAnimatedReaction, useAnimatedStyle, useSharedValue, withTiming, runOnJS, } 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' const SCROLLED_DOWN_LIMIT = 200 interface PagerWithHeaderChildParams { headerHeight: number onScroll: OnScrollHandler isScrolledDown: boolean } 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 {isMobile} = useWebMediaQueries() const [currentPage, setCurrentPage] = React.useState(0) const scrollYs = React.useRef>({}) const scrollY = useSharedValue(scrollYs.current[currentPage] || 0) const [tabBarHeight, setTabBarHeight] = React.useState(0) const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0) const [isScrolledDown, setIsScrolledDown] = React.useState( scrollYs.current[currentPage] > SCROLLED_DOWN_LIMIT, ) const headerHeight = headerOnlyHeight + tabBarHeight // react to scroll updates function onScrollUpdate(v: number) { // track each page's current scroll position scrollYs.current[currentPage] = Math.min(v, headerOnlyHeight) // update the 'is scrolled down' value setIsScrolledDown(v > SCROLLED_DOWN_LIMIT) } useAnimatedReaction( () => scrollY.value, v => runOnJS(onScrollUpdate)(v), ) // 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], ) // render the the header and tab bar const headerTransform = useAnimatedStyle( () => ({ transform: [ { translateY: Math.min( Math.min(scrollY.value, headerOnlyHeight) * -1, 0, ), }, ], }), [scrollY, headerHeight, tabBarHeight], ) const renderTabBar = React.useCallback( (props: RenderTabBarFnProps) => { return ( {renderHeader?.()} ) }, [ items, isHeaderReady, renderHeader, headerTransform, currentPage, onCurrentPageSelected, isMobile, onTabBarLayout, onHeaderOnlyLayout, ], ) // props to pass into children render functions function onScrollWorklet(e: NativeScrollEvent) { 'worklet' scrollY.value = e.contentOffset.y } const onPageSelectedInner = React.useCallback( (index: number) => { setCurrentPage(index) onPageSelected?.(index) }, [onPageSelected, setCurrentPage], ) const onPageSelecting = React.useCallback( (index: number) => { setCurrentPage(index) if (scrollY.value > headerHeight) { scrollY.value = headerHeight } scrollY.value = withTiming(scrollYs.current[index] || 0, { duration: 170, easing: Easing.inOut(Easing.quad), }) }, [scrollY, setCurrentPage, scrollYs, headerHeight], ) return ( {toArray(children) .filter(Boolean) .map((child, i) => { let output = null if ( child != null && // Defer showing content until we know it won't jump. isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0 ) { output = child({ headerHeight, isScrolledDown, onScroll: { onScroll: i === currentPage ? onScrollWorklet : noop, }, }) } // Pager children must be noncollapsible plain s. return ( {output} ) })} ) }, ) 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] }