Requests / src /components /ui /combobox.tsx
armand0e's picture
Fix dropdown for HF models
46d4f6c
"use client";
import * as React from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
interface ComboboxProps {
options: { id: string; name: string }[];
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
searchPlaceholder?: string;
emptyMessage?: string;
loading?: boolean;
searchValue?: string;
onSearchValueChange?: (value: string) => void;
disableLocalFilter?: boolean;
}
export function Combobox({
options,
value,
onValueChange,
placeholder = "Select option...",
searchPlaceholder = "Search...",
emptyMessage = "No results found.",
loading = false,
searchValue,
onSearchValueChange,
disableLocalFilter = false,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
const [internalSearch, setInternalSearch] = React.useState("");
const search = searchValue ?? internalSearch;
const filteredOptions = React.useMemo(() => {
if (disableLocalFilter) return options;
if (!search) return options;
const lower = search.toLowerCase();
return options.filter(
(option) =>
option.name.toLowerCase().includes(lower) ||
option.id.toLowerCase().includes(lower)
);
}, [disableLocalFilter, options, search]);
const selectedOption = options.find((opt) => opt.id === value);
const selectedLabel = selectedOption?.name ?? (value ? value : placeholder);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between font-normal"
>
<span className="truncate">
{selectedLabel}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0" align="start">
<div className="p-2">
<Input
placeholder={searchPlaceholder}
value={search}
onChange={(e) => {
const next = e.target.value;
if (onSearchValueChange) {
onSearchValueChange(next);
} else {
setInternalSearch(next);
}
}}
className="h-9"
/>
</div>
<div className="max-h-[300px] overflow-y-auto">
{loading ? (
<div className="py-6 text-center text-sm text-muted-foreground">
Loading...
</div>
) : filteredOptions.length === 0 ? (
<div className="py-6 text-center text-sm text-muted-foreground">
{emptyMessage}
</div>
) : (
<div className="p-1">
{filteredOptions.map((option) => (
<button
key={option.id}
onClick={() => {
onValueChange(option.id === value ? "" : option.id);
setOpen(false);
if (onSearchValueChange) {
onSearchValueChange("");
} else {
setInternalSearch("");
}
}}
className={cn(
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground",
value === option.id && "bg-accent"
)}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
{value === option.id && <Check className="h-4 w-4" />}
</span>
<span className="truncate">{option.name}</span>
</button>
))}
</div>
)}
</div>
</PopoverContent>
</Popover>
);
}