import {useCallback, useEffect, useRef} from 'react' import {Virtuoso, VirtuosoProps} from 'react-virtuoso' import {RefreshIndicator} from '../../internal/components/refresh-indicator' import {usePullToRefresh} from '../../internal/usePullToRefresh' import {findVirtuosoScrollableElement} from '../../internal/utils/virtuoso-dom' import {cn} from '../../lib/utils' import '../../styles/utilities.css' import {Skeleton} from '../ui/skeleton' const DEFAULT_REFRESH_PULL_THRESHOLD = 200 const ELEMENT_BIND_DELAY = 100 /** * A virtualized list component for efficiently rendering large datasets with customizable item rendering and built-in pull-to-refresh functionality. * @publicDocs */ export interface ListDocProps { /** Array of items to render */ items: T[] /** Function to render each item */ renderItem: (item: T, index: number) => React.ReactNode /** Height of the list container */ height?: string | number /** Show scrollbar (default: false) */ showScrollbar?: boolean /** Header element rendered at the top of the list */ header?: React.ReactNode /** Callback to fetch more items when scrolled to bottom */ fetchMore?: () => Promise /** Custom loading component shown while fetching more */ loadingComponent?: React.ReactNode /** Callback for pull-to-refresh */ onRefresh?: () => Promise /** Whether the list is currently refreshing */ refreshing?: boolean /** Enable pull-to-refresh gesture (default: true) */ enablePullToRefresh?: boolean } export interface ListProps extends Omit< VirtuosoProps, 'data' | 'itemContent' | 'endReached' > { items: T[] renderItem: (item: T, index: number) => React.ReactNode showScrollbar?: boolean header?: React.ReactNode fetchMore?: () => Promise loadingComponent?: React.ReactNode onRefresh?: () => Promise refreshing?: boolean enablePullToRefresh?: boolean } export function List({ items, height, renderItem, className, showScrollbar = false, header, fetchMore, loadingComponent, onRefresh, refreshing, enablePullToRefresh = true, ...virtuosoProps }: ListProps) { const inFlightFetchMoreRef = useRef | null>(null) const virtuosoRef = useRef(null) const containerRef = useRef(null) const {state: pullToRefreshState, bindToElement} = usePullToRefresh({ onRefresh, threshold: DEFAULT_REFRESH_PULL_THRESHOLD, enabled: enablePullToRefresh && Boolean(onRefresh), }) const _fetchMore = useCallback(() => { // Dedupe concurrent calls by returning the same in-flight promise if (inFlightFetchMoreRef.current) return const current = Promise.resolve(fetchMore?.()).finally(() => { // Only clear if this is still the most recent promise if (inFlightFetchMoreRef.current === current) { inFlightFetchMoreRef.current = null } }) inFlightFetchMoreRef.current = current }, [fetchMore]) const itemContent = useCallback( (index: number, item: T) => <>{renderItem(item, index)}, [renderItem] ) const Footer = useCallback(() => { if (!fetchMore) return null return loadingComponent ?? }, [loadingComponent, fetchMore]) const classNames = cn(showScrollbar ? undefined : 'no-scrollbars', className) useEffect(() => { if (containerRef.current && enablePullToRefresh && onRefresh) { let cleanup: (() => void) | undefined const findAndBind = () => { if (!containerRef.current) return const scrollableElement = findVirtuosoScrollableElement( containerRef.current ) cleanup = bindToElement(scrollableElement) } const timeoutId = setTimeout(findAndBind, ELEMENT_BIND_DELAY) return () => { clearTimeout(timeoutId) if (cleanup) cleanup() } } return undefined }, [bindToElement, enablePullToRefresh, onRefresh]) const EnhancedHeader = useCallback(() => { const effectivePullDistance = refreshing ? Math.max(pullToRefreshState.pullDistance, 140) : pullToRefreshState.pullDistance const refreshHeaderHeight = Math.min( Math.max(effectivePullDistance, 0), 140 ) return ( <> {enablePullToRefresh && onRefresh && (
)} {header &&
{header}
} ) }, [header, enablePullToRefresh, onRefresh, pullToRefreshState, refreshing]) return (
) }