diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx new file mode 100644 index 0000000000..203f771a27 --- /dev/null +++ b/src/view/com/util/List.web.tsx @@ -0,0 +1,224 @@ +import React, {memo, startTransition} from 'react' +import {FlatListProps, ScrollView, StyleSheet, View} from 'react-native' +import {addStyle} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' + +export type ListMethods = FlatList_INTERNAL +export type ListProps = Omit< + FlatListProps, + 'onScroll' // Use ScrollContext instead. +> & { + onScrolledDownChange?: (isScrolledDown: boolean) => void +} +export type ListRef = React.MutableRefObject + +function ListImpl( + { + ListHeaderComponent, + ListFooterComponent, + contentContainerStyle, + contentOffset, + data, + desktopFixedHeight, + keyExtractor, + onEndReached, + onEndReachedThreshold, + onScrolledDownChange, + renderItem, + extraData, + style, + ...props + }: ListProps, + ref: React.Ref, +) { + const pal = usePalette('default') + const {isMobile} = useWebMediaQueries() + if (!isMobile) { + contentContainerStyle = addStyle( + contentContainerStyle, + styles.containerScroll, + ) + } + + if (contentOffset && contentOffset?.y !== 0) { + // NOTE + // we use paddingTop & contentOffset to space around the floating header + // but reactnative web puts the paddingTop on the wrong element (style instead of the contentContainer) + // so we manually correct it here + // -prf + style = addStyle(style, { + paddingTop: 0, + }) + contentContainerStyle = addStyle(contentContainerStyle, { + paddingTop: Math.abs(contentOffset.y), + }) + } + + let header = null + if (ListHeaderComponent != null) { + if (typeof ListHeaderComponent === 'object') { + header = ListHeaderComponent + } else if (typeof ListHeaderComponent === 'function') { + // @ts-ignore We aren't using classes so it's a render function. + header = ListHeaderComponent() + } + } + + let footer = null + if (ListFooterComponent != null) { + if (typeof ListFooterComponent === 'object') { + footer = ListFooterComponent + } else if (typeof ListFooterComponent === 'function') { + // @ts-ignore We aren't using classes so it's a render function. + footer = ListFooterComponent() + } + } + + const nativeRef = React.useRef(null) + React.useImperativeHandle( + ref, + () => + ({ + scrollToTop() { + window.scrollTo({top: 0}) + }, + scrollToOffset({ + animated, + offset, + }: { + animated: boolean + offset: number + }) { + window.scrollTo({ + left: 0, + top: offset, + behavior: animated ? 'smooth' : 'instant', + }) + }, + } as any), // TODO: Types. + [], + ) + + const onVisible = React.useCallback(() => { + onEndReached?.({ + distanceFromEnd: onEndReachedThreshold || 0, + }) + }, [onEndReachedThreshold, onEndReached]) + + return ( + + + {header} + {(data as Array).map((item, index) => ( + + key={keyExtractor!(item, index)} + item={item} + index={index} + renderItem={renderItem} + extraData={extraData} + /> + ))} + {onEndReached && ( + + )} + {footer} + + + ) +} + +let Row = function RowImpl({ + item, + index, + renderItem, + extraData: _unused, +}: { + item: ItemT + index: number + renderItem: + | null + | undefined + | ((data: {index: number; item: any; separators: any}) => React.ReactNode) + extraData: any +}): React.ReactNode { + if (!renderItem) { + return null + } + return ( + + {renderItem({item, index, separators: null as any})} + + ) +} +Row = React.memo(Row) + +let Tail = ({ + threshold = 0, + onVisible, +}: { + threshold?: number | null | undefined + onVisible: () => void +}): React.ReactNode => { + const tailRef = React.useRef(null) + + React.useEffect(() => { + const observer = new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + onVisible() + } + }) + }, + { + rootMargin: (threshold || 0) * 100 + '%', + }, + ) + const tail: Element | null = tailRef.current! + observer.observe(tail) + return () => { + observer.unobserve(tail) + } + }, [onVisible, threshold]) + + return +} +Tail = React.memo(Tail) + +export const List = memo(React.forwardRef(ListImpl)) as ( + props: ListProps & {ref?: React.Ref}, +) => React.ReactElement + +const styles = StyleSheet.create({ + contentContainer: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + container: { + width: '100%', + maxWidth: 600, + marginLeft: 'auto', + marginRight: 'auto', + }, + containerScroll: { + width: '100%', + maxWidth: 600, + marginLeft: 'auto', + marginRight: 'auto', + }, + row: { + // @ts-ignore web + contentVisibility: 'auto', + }, + minHeightViewport: { + // @ts-ignore web only + minHeight: '100vh', + }, +}) diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index 80f87bd8ea..65cf1a5c10 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -51,18 +51,9 @@ export function CenteredView({ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl( { - data, - extraData, - contentOffset, - keyExtractor, - renderItem, - style, contentContainerStyle, - onEndReached, - onEndReachedThreshold, - onScroll: _unused, // Not supported on the web. - ListHeaderComponent, - ListFooterComponent, + style, + contentOffset, desktopFixedHeight, ...props }: React.PropsWithChildren & AddedProps>, @@ -89,144 +80,45 @@ export const FlatList_INTERNAL = React.forwardRef(function FlatListImpl( paddingTop: Math.abs(contentOffset.y), }) } - - const nativeRef = React.useRef(null) - React.useImperativeHandle( - ref, - () => - ({ - scrollToTop() { - window.scrollTo({top: 0}) - }, - scrollToOffset({ - animated, - offset, - }: { - animated: boolean - offset: number - }) { - window.scrollTo({ - left: 0, - top: offset, - behavior: animated ? 'smooth' : 'instant', - }) - }, - } as any), // TODO: Types. - [], - ) - - const onVisible = React.useCallback(() => { - onEndReached?.({ - distanceFromEnd: onEndReachedThreshold || 0, - }) - }, [onEndReachedThreshold, onEndReached]) - - let header = null - if (ListHeaderComponent != null) { - if (typeof ListHeaderComponent === 'object') { - header = ListHeaderComponent - } else if (typeof ListHeaderComponent === 'function') { - // @ts-ignore We aren't using classes so it's a render function. - header = ListHeaderComponent() + if (desktopFixedHeight) { + if (typeof desktopFixedHeight === 'number') { + // @ts-ignore Web only -prf + style = addStyle(style, { + height: `calc(100vh - ${desktopFixedHeight}px)`, + }) + } else { + style = addStyle(style, styles.fixedHeight) + } + if (!isMobile) { + // NOTE + // react native web produces *three* wrapping divs + // the first two use the `style` prop and the innermost uses the + // `contentContainerStyle`. Unfortunately the stable-gutter style + // needs to be applied to only the "middle" of these. To hack + // around this, we set data-stable-gutters which can then be + // styled in our external CSS. + // -prf + // @ts-ignore web only -prf + props.dataSet = props.dataSet || {} + // @ts-ignore web only -prf + props.dataSet.stableGutters = '1' } } - - let footer = null - if (ListFooterComponent != null) { - if (typeof ListFooterComponent === 'object') { - footer = ListFooterComponent - } else if (typeof ListFooterComponent === 'function') { - // @ts-ignore We aren't using classes so it's a render function. - footer = ListFooterComponent() - } - } - return ( - - - {header} - {(data as Array).map((item, index) => ( - - key={keyExtractor!(item, index)} - item={item} - index={index} - renderItem={renderItem} - extraData={extraData} - /> - ))} - {onEndReached && ( - - )} - {footer} - - + ) }) -let Row = function RowImpl({ - item, - index, - renderItem, - extraData: _unused, -}: { - item: ItemT - index: number - renderItem: - | null - | undefined - | ((data: {index: number; item: any; separators: any}) => React.ReactNode) - extraData: any -}): React.ReactNode { - if (!renderItem) { - return null - } - return ( - - {renderItem({item, index, separators: null as any})} - - ) -} -Row = React.memo(Row) - -let Tail = ({ - threshold = 0, - onVisible, -}: { - threshold?: number | null | undefined - onVisible: () => void -}): React.ReactNode => { - const tailRef = React.useRef(null) - - React.useEffect(() => { - const observer = new IntersectionObserver( - entries => { - entries.forEach(entry => { - if (entry.isIntersecting) { - onVisible() - } - }) - }, - { - rootMargin: (threshold || 0) * 100 + '%', - }, - ) - const tail: Element | null = tailRef.current! - observer.observe(tail) - return () => { - observer.unobserve(tail) - } - }, [onVisible, threshold]) - - return -} -Tail = React.memo(Tail) - export const ScrollView = React.forwardRef(function ScrollViewImpl( {contentContainerStyle, ...props}: React.PropsWithChildren, ref: React.Ref,