about summary refs log tree commit diff
path: root/src/view/com/pager
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/pager')
-rw-r--r--src/view/com/pager/FeedsTabBar.web.tsx11
-rw-r--r--src/view/com/pager/FeedsTabBarMobile.tsx26
-rw-r--r--src/view/com/pager/Pager.tsx1
-rw-r--r--src/view/com/pager/Pager.web.tsx51
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx15
-rw-r--r--src/view/com/pager/PagerWithHeader.web.tsx194
6 files changed, 261 insertions, 37 deletions
diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx
index 57c83f17c..385da5544 100644
--- a/src/view/com/pager/FeedsTabBar.web.tsx
+++ b/src/view/com/pager/FeedsTabBar.web.tsx
@@ -117,7 +117,7 @@ function FeedsTabBarTablet(
   return (
     // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf
     <Animated.View
-      style={[pal.view, styles.tabBar, headerMinimalShellTransform]}
+      style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
       onLayout={e => {
         headerHeight.value = e.nativeEvent.layout.height
       }}>
@@ -134,13 +134,16 @@ function FeedsTabBarTablet(
 
 const styles = StyleSheet.create({
   tabBar: {
-    position: 'absolute',
+    // @ts-ignore Web only
+    position: 'sticky',
     zIndex: 1,
     // @ts-ignore Web only -prf
-    left: 'calc(50% - 299px)',
-    width: 598,
+    left: 'calc(50% - 300px)',
+    width: 600,
     top: 0,
     flexDirection: 'row',
     alignItems: 'center',
+    borderLeftWidth: 1,
+    borderRightWidth: 1,
   },
 })
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index 2c5ba5dfb..b9959a6d9 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -20,6 +20,11 @@ import {useNavigation} from '@react-navigation/native'
 import {NavigationProp} from 'lib/routes/types'
 import {Logo} from '#/view/icons/Logo'
 
+import {IS_DEV} from '#/env'
+import {atoms} from '#/alf'
+import {Link as Link2} from '#/components/Link'
+import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
+
 export function FeedsTabBar(
   props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
 ) {
@@ -68,7 +73,7 @@ export function FeedsTabBar(
         headerHeight.value = e.nativeEvent.layout.height
       }}>
       <View style={[pal.view, styles.topBar]}>
-        <View style={[pal.view]}>
+        <View style={[pal.view, {width: 100}]}>
           <TouchableOpacity
             testID="viewHeaderDrawerBtn"
             onPress={onPressAvi}
@@ -88,7 +93,21 @@ export function FeedsTabBar(
         <View>
           <Logo width={30} />
         </View>
-        <View style={[pal.view, {width: 18}]}>
+        <View
+          style={[
+            atoms.flex_row,
+            atoms.justify_end,
+            atoms.align_center,
+            atoms.gap_md,
+            pal.view,
+            {width: 100},
+          ]}>
+          {IS_DEV && (
+            <Link2 to="/sys/debug">
+              <ColorPalette size="md" />
+            </Link2>
+          )}
+
           {hasSession && (
             <Link
               testID="viewHeaderHomeFeedPrefsBtn"
@@ -123,7 +142,8 @@ export function FeedsTabBar(
 
 const styles = StyleSheet.create({
   tabBar: {
-    position: 'absolute',
+    // @ts-ignore web-only
+    position: isWeb ? 'fixed' : 'absolute',
     zIndex: 1,
     left: 0,
     right: 0,
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index 61c3609f2..834b1c0d0 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -17,6 +17,7 @@ export interface PagerRef {
 export interface RenderTabBarFnProps {
   selectedPage: number
   onSelect?: (index: number) => void
+  tabBarAnchor?: JSX.Element | null | undefined // Ignored on native.
 }
 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
 
diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx
index 3b5e9164a..dde799e42 100644
--- a/src/view/com/pager/Pager.web.tsx
+++ b/src/view/com/pager/Pager.web.tsx
@@ -1,10 +1,12 @@
 import React from 'react'
+import {flushSync} from 'react-dom'
 import {View} from 'react-native'
 import {s} from 'lib/styles'
 
 export interface RenderTabBarFnProps {
   selectedPage: number
   onSelect?: (index: number) => void
+  tabBarAnchor?: JSX.Element
 }
 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
 
@@ -27,6 +29,8 @@ export const Pager = React.forwardRef(function PagerImpl(
   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) => setSelectedPage(index),
@@ -34,11 +38,36 @@ export const Pager = React.forwardRef(function PagerImpl(
 
   const onTabBarSelect = React.useCallback(
     (index: number) => {
-      setSelectedPage(index)
-      onPageSelected?.(index)
-      onPageSelecting?.(index)
+      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)
+        onPageSelecting?.(index)
+      })
+      if (isSticking) {
+        const restoredScrollY = scrollYs.current[index]
+        if (restoredScrollY != null) {
+          window.scrollTo(0, restoredScrollY)
+        } else {
+          window.scrollTo(0, scrollY + anchorTop)
+        }
+      }
     },
-    [setSelectedPage, onPageSelected, onPageSelecting],
+    [selectedPage, setSelectedPage, onPageSelected, onPageSelecting],
   )
 
   return (
@@ -46,21 +75,11 @@ export const Pager = React.forwardRef(function PagerImpl(
       {tabBarPosition === 'top' &&
         renderTabBar({
           selectedPage,
+          tabBarAnchor: <View ref={anchorRef} />,
           onSelect: onTabBarSelect,
         })}
       {React.Children.map(children, (child, i) => (
-        <View
-          style={
-            selectedPage === i
-              ? s.flex1
-              : {
-                  position: 'absolute',
-                  pointerEvents: 'none',
-                  // @ts-ignore web-only
-                  visibility: 'hidden',
-                }
-          }
-          key={`page-${i}`}>
+        <View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
           {child}
         </View>
       ))}
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 158940d67..279b607ad 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -18,7 +18,6 @@ import Animated, {
 } from 'react-native-reanimated'
 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {TabBar} from './TabBar'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {ListMethods} from '../util/List'
 import {ScrollProvider} from '#/lib/ScrollContext'
@@ -235,7 +234,6 @@ let PagerTabBar = ({
   onCurrentPageSelected?: (index: number) => void
   onSelect?: (index: number) => void
 }): React.ReactNode => {
-  const {isMobile} = useWebMediaQueries()
   const headerTransform = useAnimatedStyle(() => ({
     transform: [
       {
@@ -246,10 +244,7 @@ let PagerTabBar = ({
   return (
     <Animated.View
       pointerEvents="box-none"
-      style={[
-        isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
-        headerTransform,
-      ]}>
+      style={[styles.tabBarMobile, headerTransform]}>
       <View onLayout={onHeaderOnlyLayout} pointerEvents="box-none">
         {renderHeader?.()}
       </View>
@@ -325,14 +320,6 @@ const styles = StyleSheet.create({
     left: 0,
     width: '100%',
   },
-  tabBarDesktop: {
-    position: 'absolute',
-    zIndex: 1,
-    top: 0,
-    // @ts-ignore Web only -prf
-    left: 'calc(50% - 299px)',
-    width: 598,
-  },
 })
 
 function noop() {
diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx
new file mode 100644
index 000000000..0a18a9e7d
--- /dev/null
+++ b/src/view/com/pager/PagerWithHeader.web.tsx
@@ -0,0 +1,194 @@
+import * as React from 'react'
+import {FlatList, ScrollView, StyleSheet, View} from 'react-native'
+import {useAnimatedRef} from 'react-native-reanimated'
+import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
+import {TabBar} from './TabBar'
+import {usePalette} from '#/lib/hooks/usePalette'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+import {ListMethods} from '../util/List'
+
+export interface PagerWithHeaderChildParams {
+  headerHeight: number
+  isFocused: boolean
+  scrollElRef: React.MutableRefObject<FlatList<any> | 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<PagerRef, PagerWithHeaderProps>(
+  function PageWithHeaderImpl(
+    {
+      children,
+      testID,
+      items,
+      renderHeader,
+      initialPage,
+      onPageSelected,
+      onCurrentPageSelected,
+    }: PagerWithHeaderProps,
+    ref,
+  ) {
+    const [currentPage, setCurrentPage] = React.useState(0)
+
+    const renderTabBar = React.useCallback(
+      (props: RenderTabBarFnProps) => {
+        return (
+          <PagerTabBar
+            items={items}
+            renderHeader={renderHeader}
+            currentPage={currentPage}
+            onCurrentPageSelected={onCurrentPageSelected}
+            onSelect={props.onSelect}
+            tabBarAnchor={props.tabBarAnchor}
+            testID={testID}
+          />
+        )
+      },
+      [items, renderHeader, currentPage, onCurrentPageSelected, testID],
+    )
+
+    const onPageSelectedInner = React.useCallback(
+      (index: number) => {
+        setCurrentPage(index)
+        onPageSelected?.(index)
+      },
+      [onPageSelected, setCurrentPage],
+    )
+
+    const onPageSelecting = React.useCallback((index: number) => {
+      setCurrentPage(index)
+    }, [])
+
+    return (
+      <Pager
+        ref={ref}
+        testID={testID}
+        initialPage={initialPage}
+        onPageSelected={onPageSelectedInner}
+        onPageSelecting={onPageSelecting}
+        renderTabBar={renderTabBar}
+        tabBarPosition="top">
+        {toArray(children)
+          .filter(Boolean)
+          .map((child, i) => {
+            return (
+              <View key={i} collapsable={false}>
+                <PagerItem isFocused={i === currentPage} renderTab={child} />
+              </View>
+            )
+          })}
+      </Pager>
+    )
+  },
+)
+
+let PagerTabBar = ({
+  currentPage,
+  items,
+  testID,
+  renderHeader,
+  onCurrentPageSelected,
+  onSelect,
+  tabBarAnchor,
+}: {
+  currentPage: number
+  items: string[]
+  testID?: string
+  renderHeader?: () => JSX.Element
+  onCurrentPageSelected?: (index: number) => void
+  onSelect?: (index: number) => void
+  tabBarAnchor?: JSX.Element | null | undefined
+}): React.ReactNode => {
+  const pal = usePalette('default')
+  const {isMobile} = useWebMediaQueries()
+  return (
+    <>
+      <View style={[!isMobile && styles.headerContainerDesktop, pal.border]}>
+        {renderHeader?.()}
+      </View>
+      {tabBarAnchor}
+      <View
+        style={[
+          styles.tabBarContainer,
+          isMobile
+            ? styles.tabBarContainerMobile
+            : styles.tabBarContainerDesktop,
+          pal.border,
+        ]}>
+        <TabBar
+          testID={testID}
+          items={items}
+          selectedPage={currentPage}
+          onSelect={onSelect}
+          onPressSelected={onCurrentPageSelected}
+        />
+      </View>
+    </>
+  )
+}
+PagerTabBar = React.memo(PagerTabBar)
+
+function PagerItem({
+  isFocused,
+  renderTab,
+}: {
+  isFocused: boolean
+  renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
+}) {
+  const scrollElRef = useAnimatedRef()
+  if (renderTab == null) {
+    return null
+  }
+  return renderTab({
+    headerHeight: 0,
+    isFocused,
+    scrollElRef: scrollElRef as React.MutableRefObject<
+      ListMethods | ScrollView | null
+    >,
+  })
+}
+
+const styles = StyleSheet.create({
+  headerContainerDesktop: {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+    width: 600,
+    borderLeftWidth: 1,
+    borderRightWidth: 1,
+  },
+  tabBarContainer: {
+    // @ts-ignore web-only
+    position: 'sticky',
+    overflow: 'hidden',
+    top: 0,
+    zIndex: 1,
+  },
+  tabBarContainerDesktop: {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+    width: 600,
+    borderLeftWidth: 1,
+    borderRightWidth: 1,
+  },
+  tabBarContainerMobile: {
+    paddingLeft: 14,
+    paddingRight: 14,
+  },
+})
+
+function toArray<T>(v: T | T[]): T[] {
+  if (Array.isArray(v)) {
+    return v
+  }
+  return [v]
+}