'use client'; import { useCallback, useRef, useState } from 'react'; import { Loader2 } from 'lucide-react'; import type { Virtualizer } from '@tanstack/react-virtual'; import { Button } from '@/components/ui/button'; import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxList, ComboboxTrigger, ComboboxValue, } from '@/components/ui/combobox'; import type { AsyncComboboxOption, AsyncComboboxProps } from './AsyncCombobox.types'; import { VirtualComboboxList } from './VirtualComboboxList'; type VirtualizerHandle = Virtualizer; export function AsyncCombobox({ lookup, onValueChange, placeholder = 'Select option', searchPlaceholder = 'Search...', emptyMessage = 'No items found.', disabled = false, portalContainer, }: AsyncComboboxProps) { const [open, setOpen] = useState(false); const virtualizerRef = useRef(null); const { options, selectedOption, search, setSearch, isLoading, isFetchingNextPage, isError, errorMessage, hasNextPage, fetchNextPage, } = lookup; const handleOpenChange = useCallback( (nextOpen: boolean) => { setOpen(nextOpen); if (!nextOpen) { setSearch(''); } }, [setSearch], ); const handleValueChange = useCallback( (item: AsyncComboboxOption | null) => { if (item?.value) { onValueChange(item.value); } }, [onValueChange], ); const handleItemHighlighted = useCallback( ( _item: AsyncComboboxOption | undefined, event: { reason: string; index: number }, ) => { const virtualizer = virtualizerRef.current; if (!virtualizer) { return; } const { reason, index } = event; const isStart = index === 0; const isEnd = index === virtualizer.options.count - 1; const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd)); if (shouldScroll) { queueMicrotask(() => { virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' }); }); } }, [], ); const handleLoadMore = useCallback(() => { void fetchNextPage(); }, [fetchNextPage]); const isDisabled = disabled || isLoading; const triggerPlaceholder = isLoading ? 'Loading...' : placeholder; return ( item.label} isItemEqualToValue={(item, currentValue) => item.value === currentValue.value} onItemHighlighted={handleItemHighlighted} > {isLoading && !selectedOption ? ( Loading... ) : ( )} } /> {isError ? (
{errorMessage || 'Unable to load options.'}
) : ( <> {emptyMessage} )}
); }