import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { useVirtualizer } from "@tanstack/react-virtual"; import { Check, ChevronsUpDown } from "lucide-react"; import * as React from "react"; type Option = { value: string; label: string; }; interface VirtualizedCommandProps { height: string; options: Option[]; placeholder: string; selectedOption: string; onSelectOption?: (option: string) => void; } const VirtualizedCommand = ({ height, options, placeholder, selectedOption, onSelectOption, }: VirtualizedCommandProps) => { const [filteredOptions, setFilteredOptions] = React.useState(options); const [focusedIndex, setFocusedIndex] = React.useState(0); const [isKeyboardNavActive, setIsKeyboardNavActive] = React.useState(false); const parentRef = React.useRef(null); const virtualizer = useVirtualizer({ count: filteredOptions.length, getScrollElement: () => parentRef.current, estimateSize: () => 35, }); const virtualOptions = virtualizer.getVirtualItems(); const scrollToIndex = (index: number) => { virtualizer.scrollToIndex(index, { align: "center", }); }; const handleSearch = (search: string) => { setIsKeyboardNavActive(false); setFilteredOptions( options.filter((option) => option.value.toLowerCase().includes(search.toLowerCase() ?? []) ) ); }; const handleKeyDown = (event: React.KeyboardEvent) => { switch (event.key) { case "ArrowDown": { event.preventDefault(); setIsKeyboardNavActive(true); setFocusedIndex((prev) => { const newIndex = prev === -1 ? 0 : Math.min(prev + 1, filteredOptions.length - 1); scrollToIndex(newIndex); return newIndex; }); break; } case "ArrowUp": { event.preventDefault(); setIsKeyboardNavActive(true); setFocusedIndex((prev) => { const newIndex = prev === -1 ? filteredOptions.length - 1 : Math.max(prev - 1, 0); scrollToIndex(newIndex); return newIndex; }); break; } case "Enter": { event.preventDefault(); if (filteredOptions[focusedIndex]) { onSelectOption?.(filteredOptions[focusedIndex].value); } break; } default: break; } }; React.useEffect(() => { if (selectedOption) { const option = filteredOptions.find( (option) => option.value === selectedOption ); if (option) { const index = filteredOptions.indexOf(option); setFocusedIndex(index); virtualizer.scrollToIndex(index, { align: "center", }); } } }, [selectedOption, filteredOptions, virtualizer]); return ( setIsKeyboardNavActive(false)} onMouseMove={() => setIsKeyboardNavActive(false)} > No item found.
{virtualOptions.map((virtualOption) => ( !isKeyboardNavActive && setFocusedIndex(virtualOption.index) } onMouseLeave={() => !isKeyboardNavActive && setFocusedIndex(-1)} onSelect={onSelectOption} > {filteredOptions[virtualOption.index].label} ))}
); }; interface VirtualizedComboboxProps { options: string[]; searchPlaceholder?: string; width?: string; height?: string; value: string; onValueChange: (value: string) => void; } export function VirtualizedCombobox({ options, searchPlaceholder = "Search items...", value, onValueChange, width = "400px", height = "400px", }: VirtualizedComboboxProps) { const [open, setOpen] = React.useState(false); const [selectedOption, setSelectedOption] = React.useState(""); return ( ({ value: option, label: option }))} placeholder={searchPlaceholder} selectedOption={value} onSelectOption={(currentValue) => { onValueChange(currentValue); setOpen(false); }} /> ); }