import * as React from 'react'; import { Ref, useRef, useImperativeHandle, useState, useCallback } from 'react'; import { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView, ScrollViewProps, View, ViewProps } from 'react-native'; interface RenderEvent { item: T; index: number; } interface ScrollToOptions { index: number; viewPosition: number; animated: boolean; } export interface NestableFlatListControl { scrollToIndex(options: ScrollToOptions): void; } interface CellRendererProps { index: number; item: T; children: React.ReactNode; } // For compatibility, these props should be mostly compatible with the // native FlatList props: interface Props extends ScrollViewProps { ref: Ref; data: T[]; itemHeight: number; CellRendererComponent?: React.ComponentType>; renderItem: (event: RenderEvent)=> React.ReactNode; keyExtractor: (item: T)=> string; extraData: unknown; // Additional props. // The contentWrapperProps can be used to improve accessibility by // applying certain content roles to the that directly contains // the list's content. At least on web, applying these props directly to the ScrollView may // not work due to additional s added by React Native. contentWrapperProps?: ViewProps; } // This component allows working around restrictions on nesting React Native's built-in // within s. For the most part, this component's interface should // be compatible with the API. // // See https://github.com/facebook/react-native/issues/31697. const NestableFlatList = function({ ref, itemHeight, renderItem, keyExtractor, data, CellRendererComponent = React.Fragment, contentWrapperProps, ...rest }: Props) { const scrollViewRef = useRef(null); const [scroll, setScroll] = useState(0); const [listHeight, setListHeight] = useState(0); useImperativeHandle(ref, () => { return { scrollToIndex: ({ index, animated, viewPosition }) => { const offset = Math.max(0, index * itemHeight - viewPosition * listHeight); scrollViewRef.current.scrollTo({ y: offset, animated, }); // onScroll events don't seem to be sent when scrolling with .scrollTo. // The scroll offset needs to be updated manually: setScroll(offset); }, }; }, [itemHeight, listHeight]); const onScroll = useCallback((event: NativeSyntheticEvent) => { setScroll(event.nativeEvent.contentOffset.y); }, []); const onLayout = useCallback((event: LayoutChangeEvent) => { setListHeight(event.nativeEvent.layout.height); }, []); const bufferSize = 10; const visibleStartIndex = Math.min(Math.floor(scroll / itemHeight), data.length); const visibleEndIndex = Math.ceil((scroll + listHeight) / itemHeight); const startIndex = Math.max(0, visibleStartIndex - bufferSize); const maximumIndex = data.length - 1; const endIndex = Math.min(visibleEndIndex + bufferSize, maximumIndex); const paddingTop = startIndex * itemHeight; const paddingBottom = (maximumIndex - endIndex) * itemHeight; const renderVisibleItems = () => { const result: React.ReactNode[] = []; for (let i = startIndex; i <= endIndex; i++) { result.push( {renderItem({ item: data[i], index: i })} , ); } return result; }; return {renderVisibleItems()} ; }; export default NestableFlatList;