diff options
Diffstat (limited to 'src/view/com/util/List.web.tsx')
-rw-r--r-- | src/view/com/util/List.web.tsx | 201 |
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)} /> |