about summary refs log tree commit diff
path: root/src/view/com/util/List.web.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/util/List.web.tsx')
-rw-r--r--src/view/com/util/List.web.tsx201
1 files changed, 172 insertions, 29 deletions
diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx
index 936bac198..9bea2d795 100644
--- a/src/view/com/util/List.web.tsx
+++ b/src/view/com/util/List.web.tsx
@@ -1,11 +1,13 @@
-import React, {isValidElement, memo, useRef, startTransition} from 'react'
+import React, {isValidElement, memo, startTransition, useRef} from 'react'
 import {FlatListProps, StyleSheet, View, ViewProps} from 'react-native'
-import {addStyle} from 'lib/styles'
+import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes'
+
+import {batchedUpdates} from '#/lib/batchedUpdates'
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {useScrollHandlers} from '#/lib/ScrollContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useScrollHandlers} from '#/lib/ScrollContext'
-import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {batchedUpdates} from '#/lib/batchedUpdates'
+import {addStyle} from 'lib/styles'
 
 export type ListMethods = any // TODO: Better types.
 export type ListProps<ItemT> = Omit<
@@ -19,6 +21,7 @@ export type ListProps<ItemT> = Omit<
   refreshing?: boolean
   onRefresh?: () => void
   desktopFixedHeight: any // TODO: Better types.
+  containWeb?: boolean
 }
 export type ListRef = React.MutableRefObject<any | null> // TODO: Better types.
 
@@ -26,12 +29,15 @@ function ListImpl<ItemT>(
   {
     ListHeaderComponent,
     ListFooterComponent,
+    containWeb,
     contentContainerStyle,
     data,
     desktopFixedHeight,
     headerOffset,
     keyExtractor,
     refreshing: _unsupportedRefreshing,
+    onStartReached,
+    onStartReachedThreshold = 0,
     onEndReached,
     onEndReachedThreshold = 0,
     onRefresh: _unsupportedOnRefresh,
@@ -80,14 +86,88 @@ function ListImpl<ItemT>(
     })
   }
 
-  const nativeRef = React.useRef(null)
+  const getScrollableNode = React.useCallback(() => {
+    if (containWeb) {
+      const element = nativeRef.current as HTMLDivElement | null
+      if (!element) return
+
+      return {
+        get scrollWidth() {
+          return element.scrollWidth
+        },
+        get scrollHeight() {
+          return element.scrollHeight
+        },
+        get clientWidth() {
+          return element.clientWidth
+        },
+        get clientHeight() {
+          return element.clientHeight
+        },
+        get scrollY() {
+          return element.scrollTop
+        },
+        get scrollX() {
+          return element.scrollLeft
+        },
+        scrollTo(options?: ScrollToOptions) {
+          element.scrollTo(options)
+        },
+        scrollBy(options: ScrollToOptions) {
+          element.scrollBy(options)
+        },
+        addEventListener(event: string, handler: any) {
+          element.addEventListener(event, handler)
+        },
+        removeEventListener(event: string, handler: any) {
+          element.removeEventListener(event, handler)
+        },
+      }
+    } else {
+      return {
+        get scrollWidth() {
+          return document.documentElement.scrollWidth
+        },
+        get scrollHeight() {
+          return document.documentElement.scrollHeight
+        },
+        get clientWidth() {
+          return window.innerWidth
+        },
+        get clientHeight() {
+          return window.innerHeight
+        },
+        get scrollY() {
+          return window.scrollY
+        },
+        get scrollX() {
+          return window.scrollX
+        },
+        scrollTo(options: ScrollToOptions) {
+          window.scrollTo(options)
+        },
+        scrollBy(options: ScrollToOptions) {
+          window.scrollBy(options)
+        },
+        addEventListener(event: string, handler: any) {
+          window.addEventListener(event, handler)
+        },
+        removeEventListener(event: string, handler: any) {
+          window.removeEventListener(event, handler)
+        },
+      }
+    }
+  }, [containWeb])
+
+  const nativeRef = React.useRef<HTMLDivElement>(null)
   React.useImperativeHandle(
     ref,
     () =>
       ({
         scrollToTop() {
-          window.scrollTo({top: 0})
+          getScrollableNode()?.scrollTo({top: 0})
         },
+
         scrollToOffset({
           animated,
           offset,
@@ -95,46 +175,74 @@ function ListImpl<ItemT>(
           animated: boolean
           offset: number
         }) {
-          window.scrollTo({
+          getScrollableNode()?.scrollTo({
             left: 0,
             top: offset,
             behavior: animated ? 'smooth' : 'instant',
           })
         },
+        scrollToEnd({animated = true}: {animated?: boolean}) {
+          const element = getScrollableNode()
+          element?.scrollTo({
+            left: 0,
+            top: element.scrollHeight,
+            behavior: animated ? 'smooth' : 'instant',
+          })
+        },
       } as any), // TODO: Better types.
-    [],
+    [getScrollableNode],
   )
 
-  // --- onContentSizeChange ---
+  // --- onContentSizeChange, maintainVisibleContentPosition ---
   const containerRef = useRef(null)
   useResizeObserver(containerRef, onContentSizeChange)
 
   // --- onScroll ---
   const [isInsideVisibleTree, setIsInsideVisibleTree] = React.useState(false)
-  const handleWindowScroll = useNonReactiveCallback(() => {
-    if (isInsideVisibleTree) {
-      contextScrollHandlers.onScroll?.(
-        {
-          contentOffset: {
-            x: Math.max(0, window.scrollX),
-            y: Math.max(0, window.scrollY),
-          },
-        } as any, // TODO: Better types.
-        null as any,
-      )
-    }
+  const handleScroll = useNonReactiveCallback(() => {
+    if (!isInsideVisibleTree) return
+
+    const element = getScrollableNode()
+    contextScrollHandlers.onScroll?.(
+      {
+        contentOffset: {
+          x: Math.max(0, element?.scrollX ?? 0),
+          y: Math.max(0, element?.scrollY ?? 0),
+        },
+        layoutMeasurement: {
+          width: element?.clientWidth,
+          height: element?.clientHeight,
+        },
+        contentSize: {
+          width: element?.scrollWidth,
+          height: element?.scrollHeight,
+        },
+      } as Exclude<
+        ReanimatedScrollEvent,
+        | 'velocity'
+        | 'eventName'
+        | 'zoomScale'
+        | 'targetContentOffset'
+        | 'contentInset'
+      >,
+      null as any,
+    )
   })
+
   React.useEffect(() => {
     if (!isInsideVisibleTree) {
       // Prevents hidden tabs from firing scroll events.
       // Only one list is expected to be firing these at a time.
       return
     }
-    window.addEventListener('scroll', handleWindowScroll)
+
+    const element = getScrollableNode()
+
+    element?.addEventListener('scroll', handleScroll)
     return () => {
-      window.removeEventListener('scroll', handleWindowScroll)
+      element?.removeEventListener('scroll', handleScroll)
     }
-  }, [isInsideVisibleTree, handleWindowScroll])
+  }, [isInsideVisibleTree, handleScroll, containWeb, getScrollableNode])
 
   // --- onScrolledDownChange ---
   const isScrolledDown = useRef(false)
@@ -148,6 +256,17 @@ function ListImpl<ItemT>(
     }
   }
 
+  // --- onStartReached ---
+  const onHeadVisibilityChange = useNonReactiveCallback(
+    (isHeadVisible: boolean) => {
+      if (isHeadVisible) {
+        onStartReached?.({
+          distanceFromStart: onStartReachedThreshold || 0,
+        })
+      }
+    },
+  )
+
   // --- onEndReached ---
   const onTailVisibilityChange = useNonReactiveCallback(
     (isTailVisible: boolean) => {
@@ -160,7 +279,17 @@ function ListImpl<ItemT>(
   )
 
   return (
-    <View {...props} style={style} ref={nativeRef}>
+    <View
+      {...props}
+      style={[
+        style,
+        containWeb && {
+          flex: 1,
+          // @ts-expect-error web only
+          'overflow-y': 'scroll',
+        },
+      ]}
+      ref={nativeRef as any}>
       <Visibility
         onVisibleChange={setIsInsideVisibleTree}
         style={
@@ -178,9 +307,17 @@ function ListImpl<ItemT>(
           pal.border,
         ]}>
         <Visibility
+          root={containWeb ? nativeRef : null}
           onVisibleChange={handleAboveTheFoldVisibleChange}
           style={[styles.aboveTheFoldDetector, {height: headerOffset}]}
         />
+        {onStartReached && (
+          <Visibility
+            root={containWeb ? nativeRef : null}
+            onVisibleChange={onHeadVisibilityChange}
+            topMargin={(onStartReachedThreshold ?? 0) * 100 + '%'}
+          />
+        )}
         {header}
         {(data as Array<ItemT>).map((item, index) => (
           <Row<ItemT>
@@ -193,8 +330,9 @@ function ListImpl<ItemT>(
         ))}
         {onEndReached && (
           <Visibility
-            topMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
+            root={containWeb ? nativeRef : null}
             onVisibleChange={onTailVisibilityChange}
+            bottomMargin={(onEndReachedThreshold ?? 0) * 100 + '%'}
           />
         )}
         {footer}
@@ -255,11 +393,15 @@ let Row = function RowImpl<ItemT>({
 Row = React.memo(Row)
 
 let Visibility = ({
+  root,
   topMargin = '0px',
+  bottomMargin = '0px',
   onVisibleChange,
   style,
 }: {
+  root?: React.RefObject<HTMLDivElement> | null
   topMargin?: string
+  bottomMargin?: string
   onVisibleChange: (isVisible: boolean) => void
   style?: ViewProps['style']
 }): React.ReactNode => {
@@ -281,14 +423,15 @@ let Visibility = ({
 
   React.useEffect(() => {
     const observer = new IntersectionObserver(handleIntersection, {
-      rootMargin: `${topMargin} 0px 0px 0px`,
+      root: root?.current ?? null,
+      rootMargin: `${topMargin} 0px ${bottomMargin} 0px`,
     })
     const tail: Element | null = tailRef.current!
     observer.observe(tail)
     return () => {
       observer.unobserve(tail)
     }
-  }, [handleIntersection, topMargin])
+  }, [bottomMargin, handleIntersection, topMargin, root])
 
   return (
     <View ref={tailRef} style={addStyle(styles.visibilityDetector, style)} />