Files
WooNooW/admin-spa/src/components/ui/searchable-select.tsx

115 lines
3.2 KiB
TypeScript

// admin-spa/src/components/ui/searchable-select.tsx
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import {
Command,
CommandInput,
CommandList,
CommandItem,
CommandEmpty,
} from "@/components/ui/command";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
export interface Option {
value: string;
/** What to render in the button/list. Can be a string or React node. */
label: React.ReactNode;
/** Optional text used for filtering. Falls back to string label or value. */
triggerLabel?: React.ReactNode;
}
interface Props {
value?: string;
onChange?: (v: string) => void;
options: Option[];
placeholder?: string;
emptyLabel?: string;
className?: string;
disabled?: boolean;
search?: string;
onSearch?: (v: string) => void;
showCheckIndicator?: boolean;
}
export function SearchableSelect({
value,
onChange,
options,
placeholder = "Select...",
emptyLabel = "No results found.",
className,
disabled = false,
search,
onSearch,
showCheckIndicator = true,
}: Props) {
const [open, setOpen] = React.useState(false);
const selected = options.find((o) => o.value === value);
React.useEffect(() => { if (disabled && open) setOpen(false); }, [disabled, open]);
return (
<Popover open={disabled ? false : open} onOpenChange={(o) => !disabled && setOpen(o)}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className={cn("w-full justify-between", className)}
disabled={disabled}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
>
{selected ? (selected.triggerLabel ?? selected.label) : placeholder}
<ChevronsUpDown className="opacity-50 h-4 w-4 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[--radix-popover-trigger-width]"
align="start"
sideOffset={4}
>
<Command shouldFilter>
<CommandInput
className="command-palette-search"
placeholder="Search..."
value={search}
onValueChange={onSearch}
/>
<CommandList>
<CommandEmpty>{emptyLabel}</CommandEmpty>
{options.map((opt) => (
<CommandItem
key={opt.value}
value={
typeof opt.searchText === 'string' && opt.searchText.length > 0
? opt.searchText
: (typeof opt.label === 'string' ? opt.label : opt.value)
}
onSelect={() => {
onChange?.(opt.value);
setOpen(false);
}}
>
{showCheckIndicator && (
<Check
className={cn(
"mr-2 h-4 w-4 flex-shrink-0",
opt.value === value ? "opacity-100" : "opacity-0"
)}
/>
)}
{opt.label}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}