"use client"; import { useVirtualizer } from "@tanstack/react-virtual"; import { Check, ChevronsUpDown } from "lucide-react"; import * as React from "react"; 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"; export function ComboBox({ data, placeholder = "Search...", id, name, className, trigger, onValueSelect, defaultValue = "", ...props }: React.ComponentProps<"button"> & { data: { value: string; label: React.ReactNode; context?: string[] }[]; placeholder?: string; trigger?: React.ReactNode; onValueSelect?: (val: string) => void; }) { const [open, setOpen] = React.useState(false); const [value, setValue] = React.useState(defaultValue); const [query, setQuery] = React.useState(""); const list = React.useMemo( () => data.map((ent) => ({ context: ( ent.context?.join("") ?? "" + (typeof ent.label === "string" ? ent.label : "") + ent.value ).toLowerCase(), comp: ( { if (onValueSelect) onValueSelect(currentValue); else setValue(currentValue === value ? "" : currentValue); setOpen(false); }} > {ent.label} ), })), // oxlint-disable-next-line react-hooks/exhaustive-deps [data], ); const queryLower = query.toLowerCase(); React.useEffect(() => { if (defaultValue) setValue(defaultValue); }, [defaultValue]); return ( <> {trigger || ( )} None found. {list .filter((e) => e.context.includes(queryLower)) .map((e) => e.comp)} ); } export function VirtualizedComboBox({ data, placeholder = "Search...", id, name, className, trigger, onValueSelect, defaultValue = "", itemHeight = 36, ...props }: React.ComponentProps<"button"> & { data: { value: string; label: React.ReactNode; context?: string[] }[]; placeholder?: string; trigger?: React.ReactNode; onValueSelect?: (val: string) => void; itemHeight?: number; }) { const [open, setOpen] = React.useState(false); const [value, setValue] = React.useState(defaultValue); const [query, setQuery] = React.useState(""); const filtered = React.useMemo(() => { const q = query.toLowerCase(); if (!q) return data; return data.filter((ent) => { const ctx = ( ent.context?.join("") ?? "" + (typeof ent.label === "string" ? ent.label : "") + ent.value ).toLowerCase(); return ctx.includes(q); }); }, [data, query]); const listRef = React.useRef(null); const [scrollEl, setScrollEl] = React.useState(null); React.useLayoutEffect(() => { setTimeout(() => { setScrollEl(listRef.current); }, 100); }, [open, listRef]); const virtualizer = useVirtualizer({ count: filtered.length, getScrollElement: () => scrollEl, estimateSize: () => itemHeight, overscan: 5, }); React.useEffect(() => { if (defaultValue) setValue(defaultValue); }, [defaultValue]); const visibleItems = scrollEl ? virtualizer.getVirtualItems() : []; return ( <> {trigger || ( )}
e.stopPropagation()} > {filtered.length === 0 ? (
None found.
) : (
{visibleItems.map((virtualItem) => { const item = filtered[virtualItem.index]; return (
{ if (onValueSelect) onValueSelect(currentValue); else setValue( currentValue === value ? "" : currentValue, ); setOpen(false); }} > {item.label}
); })}
)}
); }