about summary refs log tree commit diff
path: root/src/view/com/pager/PagerWithHeader.tsx
diff options
context:
space:
mode:
authorhailey <me@haileyok.com>2025-06-12 10:46:22 -0700
committerGitHub <noreply@github.com>2025-06-12 10:46:22 -0700
commit477e5f4ecfaa0007aeed90b51274c78a730c1a9e (patch)
tree45cd5cfff9eab1bd52b5ade6c60efebe3cc5e6b6 /src/view/com/pager/PagerWithHeader.tsx
parenta26b20b56cd0ac80f625a5eb5136b805b9341e8d (diff)
downloadvoidsky-477e5f4ecfaa0007aeed90b51274c78a730c1a9e.tar.zst
new arch (#8295)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Charlotte Som <charlotte@som.codes>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view/com/pager/PagerWithHeader.tsx')
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx315
1 files changed, 155 insertions, 160 deletions
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 1746d2ca1..57aaac074 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -1,17 +1,16 @@
-import * as React from 'react'
+import {memo, useCallback, useEffect, useRef, useState} from 'react'
 import {
-  LayoutChangeEvent,
-  NativeScrollEvent,
-  ScrollView,
+  type LayoutChangeEvent,
+  type NativeScrollEvent,
+  type ScrollView,
   StyleSheet,
   View,
 } from 'react-native'
 import Animated, {
-  AnimatedRef,
-  runOnJS,
+  type AnimatedRef,
   runOnUI,
   scrollTo,
-  SharedValue,
+  type SharedValue,
   useAnimatedRef,
   useAnimatedStyle,
   useSharedValue,
@@ -20,9 +19,13 @@ import Animated, {
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {ScrollProvider} from '#/lib/ScrollContext'
 import {isIOS} from '#/platform/detection'
-import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
+import {
+  Pager,
+  type PagerRef,
+  type RenderTabBarFnProps,
+} from '#/view/com/pager/Pager'
 import {useTheme} from '#/alf'
-import {ListMethods} from '../util/List'
+import {type ListMethods} from '../util/List'
 import {PagerHeaderProvider} from './PagerHeaderContext'
 import {TabBar} from './TabBar'
 
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
 }
 
 export interface PagerWithHeaderProps {
+  ref?: React.Ref<PagerRef>
   testID?: string
   children:
     | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
@@ -49,97 +53,94 @@ export interface PagerWithHeaderProps {
   onCurrentPageSelected?: (index: number) => void
   allowHeaderOverScroll?: boolean
 }
-export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
-  function PageWithHeaderImpl(
-    {
-      children,
-      testID,
+export function PagerWithHeader({
+  ref,
+  children,
+  testID,
+  items,
+  isHeaderReady,
+  renderHeader,
+  initialPage,
+  onPageSelected,
+  onCurrentPageSelected,
+  allowHeaderOverScroll,
+}: PagerWithHeaderProps) {
+  const [currentPage, setCurrentPage] = useState(0)
+  const [tabBarHeight, setTabBarHeight] = useState(0)
+  const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0)
+  const scrollY = useSharedValue(0)
+  const headerHeight = headerOnlyHeight + tabBarHeight
+
+  // capture the header bar sizing
+  const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
+    const height = evt.nativeEvent.layout.height
+    if (height > 0) {
+      // The rounding is necessary to prevent jumps on iOS
+      setTabBarHeight(Math.round(height * 2) / 2)
+    }
+  })
+  const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
+    if (height > 0) {
+      // The rounding is necessary to prevent jumps on iOS
+      setHeaderOnlyHeight(Math.round(height * 2) / 2)
+    }
+  })
+
+  const renderTabBar = useCallback(
+    (props: RenderTabBarFnProps) => {
+      return (
+        <PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}>
+          <PagerTabBar
+            headerOnlyHeight={headerOnlyHeight}
+            items={items}
+            isHeaderReady={isHeaderReady}
+            renderHeader={renderHeader}
+            currentPage={currentPage}
+            onCurrentPageSelected={onCurrentPageSelected}
+            onTabBarLayout={onTabBarLayout}
+            onHeaderOnlyLayout={onHeaderOnlyLayout}
+            onSelect={props.onSelect}
+            scrollY={scrollY}
+            testID={testID}
+            allowHeaderOverScroll={allowHeaderOverScroll}
+            dragProgress={props.dragProgress}
+            dragState={props.dragState}
+          />
+        </PagerHeaderProvider>
+      )
+    },
+    [
+      headerOnlyHeight,
       items,
       isHeaderReady,
       renderHeader,
-      initialPage,
-      onPageSelected,
+      currentPage,
       onCurrentPageSelected,
+      onTabBarLayout,
+      onHeaderOnlyLayout,
+      scrollY,
+      testID,
       allowHeaderOverScroll,
-    }: PagerWithHeaderProps,
-    ref,
-  ) {
-    const [currentPage, setCurrentPage] = React.useState(0)
-    const [tabBarHeight, setTabBarHeight] = React.useState(0)
-    const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
-    const scrollY = useSharedValue(0)
-    const headerHeight = headerOnlyHeight + tabBarHeight
-
-    // capture the header bar sizing
-    const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
-      const height = evt.nativeEvent.layout.height
-      if (height > 0) {
-        // The rounding is necessary to prevent jumps on iOS
-        setTabBarHeight(Math.round(height * 2) / 2)
-      }
-    })
-    const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
-      if (height > 0) {
-        // The rounding is necessary to prevent jumps on iOS
-        setHeaderOnlyHeight(Math.round(height * 2) / 2)
-      }
-    })
-
-    const renderTabBar = React.useCallback(
-      (props: RenderTabBarFnProps) => {
-        return (
-          <PagerHeaderProvider
-            scrollY={scrollY}
-            headerHeight={headerOnlyHeight}>
-            <PagerTabBar
-              headerOnlyHeight={headerOnlyHeight}
-              items={items}
-              isHeaderReady={isHeaderReady}
-              renderHeader={renderHeader}
-              currentPage={currentPage}
-              onCurrentPageSelected={onCurrentPageSelected}
-              onTabBarLayout={onTabBarLayout}
-              onHeaderOnlyLayout={onHeaderOnlyLayout}
-              onSelect={props.onSelect}
-              scrollY={scrollY}
-              testID={testID}
-              allowHeaderOverScroll={allowHeaderOverScroll}
-              dragProgress={props.dragProgress}
-              dragState={props.dragState}
-            />
-          </PagerHeaderProvider>
-        )
-      },
-      [
-        headerOnlyHeight,
-        items,
-        isHeaderReady,
-        renderHeader,
-        currentPage,
-        onCurrentPageSelected,
-        onTabBarLayout,
-        onHeaderOnlyLayout,
-        scrollY,
-        testID,
-        allowHeaderOverScroll,
-      ],
-    )
+    ],
+  )
 
-    const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
-    const registerRef = React.useCallback(
-      (scrollRef: AnimatedRef<any> | null, atIndex: number) => {
-        scrollRefs.modify(refs => {
-          'worklet'
-          refs[atIndex] = scrollRef
-          return refs
-        })
-      },
-      [scrollRefs],
-    )
+  const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
+  const registerRef = useCallback(
+    (scrollRef: AnimatedRef<any> | null, atIndex: number) => {
+      scrollRefs.modify(refs => {
+        'worklet'
+        refs[atIndex] = scrollRef
+        return refs
+      })
+    },
+    [scrollRefs],
+  )
 
-    const lastForcedScrollY = useSharedValue(0)
-    const adjustScrollForOtherPages = () => {
+  const lastForcedScrollY = useSharedValue(0)
+  const adjustScrollForOtherPages = useCallback(
+    (scrollState: 'idle' | 'dragging' | 'settling') => {
       'worklet'
+      if (scrollState !== 'dragging') return
       const currentScrollY = scrollY.get()
       const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
       if (lastForcedScrollY.get() !== forcedScrollY) {
@@ -152,75 +153,69 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
           }
         }
       }
-    }
+    },
+    [currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY],
+  )
 
-    const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
-      null,
-    )
-    const queueThrottledOnScroll = useNonReactiveCallback(() => {
-      if (!throttleTimeout.current) {
-        throttleTimeout.current = setTimeout(() => {
-          throttleTimeout.current = null
-          runOnUI(adjustScrollForOtherPages)()
-        }, 80 /* Sync often enough you're unlikely to catch it unsynced */)
+  const onScrollWorklet = useCallback(
+    (e: NativeScrollEvent) => {
+      'worklet'
+      const nextScrollY = e.contentOffset.y
+      // HACK: onScroll is reporting some strange values on load (negative header height).
+      // Highly improbable that you'd be overscrolled by over 400px -
+      // in fact, I actually can't do it, so let's just ignore those. -sfn
+      const isPossiblyInvalid =
+        headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
+      if (!isPossiblyInvalid) {
+        scrollY.set(nextScrollY)
       }
-    })
+    },
+    [scrollY, headerHeight],
+  )
 
-    const onScrollWorklet = React.useCallback(
-      (e: NativeScrollEvent) => {
-        'worklet'
-        const nextScrollY = e.contentOffset.y
-        // HACK: onScroll is reporting some strange values on load (negative header height).
-        // Highly improbable that you'd be overscrolled by over 400px -
-        // in fact, I actually can't do it, so let's just ignore those. -sfn
-        const isPossiblyInvalid =
-          headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
-        if (!isPossiblyInvalid) {
-          scrollY.set(nextScrollY)
-          runOnJS(queueThrottledOnScroll)()
-        }
-      },
-      [scrollY, queueThrottledOnScroll, headerHeight],
-    )
+  const onPageSelectedInner = useCallback(
+    (index: number) => {
+      setCurrentPage(index)
+      onPageSelected?.(index)
+    },
+    [onPageSelected, setCurrentPage],
+  )
 
-    const onPageSelectedInner = React.useCallback(
-      (index: number) => {
-        setCurrentPage(index)
-        onPageSelected?.(index)
-      },
-      [onPageSelected, setCurrentPage],
-    )
+  const onTabPressed = useCallback(() => {
+    runOnUI(adjustScrollForOtherPages)('dragging')
+  }, [adjustScrollForOtherPages])
 
-    return (
-      <Pager
-        ref={ref}
-        testID={testID}
-        initialPage={initialPage}
-        onPageSelected={onPageSelectedInner}
-        renderTabBar={renderTabBar}>
-        {toArray(children)
-          .filter(Boolean)
-          .map((child, i) => {
-            const isReady =
-              isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
-            return (
-              <View key={i} collapsable={false}>
-                <PagerItem
-                  headerHeight={headerHeight}
-                  index={i}
-                  isReady={isReady}
-                  isFocused={i === currentPage}
-                  onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
-                  registerRef={registerRef}
-                  renderTab={child}
-                />
-              </View>
-            )
-          })}
-      </Pager>
-    )
-  },
-)
+  return (
+    <Pager
+      ref={ref}
+      testID={testID}
+      initialPage={initialPage}
+      onTabPressed={onTabPressed}
+      onPageSelected={onPageSelectedInner}
+      renderTabBar={renderTabBar}
+      onPageScrollStateChanged={adjustScrollForOtherPages}>
+      {toArray(children)
+        .filter(Boolean)
+        .map((child, i) => {
+          const isReady =
+            isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
+          return (
+            <View key={i} collapsable={false}>
+              <PagerItem
+                headerHeight={headerHeight}
+                index={i}
+                isReady={isReady}
+                isFocused={i === currentPage}
+                onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
+                registerRef={registerRef}
+                renderTab={child}
+              />
+            </View>
+          )
+        })}
+    </Pager>
+  )
+}
 
 let PagerTabBar = ({
   currentPage,
@@ -258,7 +253,7 @@ let PagerTabBar = ({
   dragState: SharedValue<'idle' | 'dragging' | 'settling'>
 }): React.ReactNode => {
   const t = useTheme()
-  const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
+  const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
   const headerTransform = useAnimatedStyle(() => {
     const translateY =
       Math.min(
@@ -275,7 +270,7 @@ let PagerTabBar = ({
       ],
     }
   })
-  const headerRef = React.useRef(null)
+  const headerRef = useRef(null)
   return (
     <Animated.View
       pointerEvents={isIOS ? 'auto' : 'box-none'}
@@ -327,7 +322,7 @@ let PagerTabBar = ({
     </Animated.View>
   )
 }
-PagerTabBar = React.memo(PagerTabBar)
+PagerTabBar = memo(PagerTabBar)
 
 function PagerItem({
   headerHeight,
@@ -348,7 +343,7 @@ function PagerItem({
 }) {
   const scrollElRef = useAnimatedRef()
 
-  React.useEffect(() => {
+  useEffect(() => {
     registerRef(scrollElRef, index)
     return () => {
       registerRef(null, index)