'use client'; import { useCallback, useEffect, useImperativeHandle, useRef, type RefObject, } from 'react'; import { Combobox } from '@base-ui/react/combobox'; import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual'; import { Loader2 } from 'lucide-react'; import { ComboboxItem } from '@/components/ui/combobox'; import { cn } from '@/lib/utils'; import type { AsyncSelectOption } from './AsyncSelect.types'; type VirtualizerHandle = Virtualizer; const LOAD_MORE_THRESHOLD = 3; interface VirtualSelectListProps { open: boolean; virtualizerRef: RefObject; hasNextPage: boolean; isFetchingNextPage: boolean; onLoadMore: () => void; estimateSize?: number; } export function VirtualSelectList({ open, virtualizerRef, hasNextPage, isFetchingNextPage, onLoadMore, estimateSize = 36, }: VirtualSelectListProps) { const filteredItems = Combobox.useFilteredItems(); const scrollElementRef = useRef(null); const itemCount = filteredItems.length + (hasNextPage ? 1 : 0); const virtualizer = useVirtualizer({ enabled: open, count: itemCount, getScrollElement: () => scrollElementRef.current, estimateSize: () => estimateSize, overscan: 8, paddingStart: 4, paddingEnd: 4, }); useImperativeHandle(virtualizerRef, () => virtualizer, [virtualizer]); const handleScrollElementRef = useCallback( (element: HTMLDivElement | null) => { scrollElementRef.current = element; if (element) { virtualizer.measure(); } }, [virtualizer], ); const virtualItems = virtualizer.getVirtualItems(); const totalSize = virtualizer.getTotalSize(); const lastVirtualIndex = virtualItems[virtualItems.length - 1]?.index ?? -1; useEffect(() => { if (lastVirtualIndex < 0) { return; } if ( lastVirtualIndex >= filteredItems.length - LOAD_MORE_THRESHOLD && hasNextPage && !isFetchingNextPage ) { onLoadMore(); } }, [ filteredItems.length, hasNextPage, isFetchingNextPage, lastVirtualIndex, onLoadMore, ]); if (!filteredItems.length && !hasNextPage) { return null; } return (
{virtualItems.map((virtualItem) => { const isLoaderRow = virtualItem.index >= filteredItems.length; const item = filteredItems[virtualItem.index]; return (
{isLoaderRow ? (
) : ( {item.label} )}
); })}
); }