| | "use client" |
| |
|
| | 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 { Check, ChevronsUpDown, X } from "lucide-react" |
| | import * as React from "react" |
| |
|
| | export interface ComboboxOption { |
| | value: string |
| | label: string |
| | } |
| |
|
| | interface ComboboxProps { |
| | options: ComboboxOption[] |
| | value?: string |
| | onValueChange?: (value: string) => void |
| | placeholder?: string |
| | searchPlaceholder?: string |
| | emptyText?: string |
| | className?: string |
| | } |
| |
|
| | export function Combobox({ |
| | options, |
| | value, |
| | onValueChange, |
| | placeholder = "Select option...", |
| | searchPlaceholder = "Search...", |
| | emptyText = "No option found.", |
| | className, |
| | }: ComboboxProps) { |
| | const [open, setOpen] = React.useState(false) |
| |
|
| | const handleClear = (e: React.MouseEvent) => { |
| | e.stopPropagation() |
| | onValueChange?.("") |
| | } |
| |
|
| | return ( |
| | <Popover open={open} onOpenChange={setOpen}> |
| | <PopoverTrigger asChild> |
| | <Button |
| | variant="outline" |
| | role="combobox" |
| | aria-expanded={open} |
| | className={cn("w-full justify-between overflow-hidden !whitespace-normal", className)} |
| | > |
| | <span className="truncate min-w-0 flex-1 text-left"> |
| | {value ? options.find((option) => option.value === value)?.label : ( |
| | <span className="text-muted-foreground">{placeholder}</span> |
| | )} |
| | </span> |
| | <div className="ml-2 flex items-center gap-1 shrink-0"> |
| | {value && ( |
| | <span |
| | role="button" |
| | onClick={handleClear} |
| | className="opacity-50 hover:opacity-100 hover:text-destructive rounded-sm p-0.5" |
| | > |
| | <X className="size-3" /> |
| | </span> |
| | )} |
| | <ChevronsUpDown className="size-4 opacity-50" /> |
| | </div> |
| | </Button> |
| | </PopoverTrigger> |
| | <PopoverContent className="w-full p-0" align="start"> |
| | <Command |
| | filter={(value, search) => { |
| | const option = options.find((opt) => opt.value === value) |
| | if (!option) return 0 |
| | return option.label.toLowerCase().includes(search.toLowerCase()) ? 1 : 0 |
| | }} |
| | > |
| | <CommandInput placeholder={searchPlaceholder} /> |
| | <CommandList> |
| | <CommandEmpty>{emptyText}</CommandEmpty> |
| | <CommandGroup> |
| | {options.map((option) => ( |
| | <CommandItem |
| | key={option.value} |
| | value={option.value} |
| | onSelect={(currentValue) => { |
| | onValueChange?.(currentValue === value ? "" : currentValue) |
| | setOpen(false) |
| | }} |
| | > |
| | <Check className={cn("mr-2 size-4", value === option.value ? "opacity-100" : "opacity-0")} /> |
| | {option.label} |
| | </CommandItem> |
| | ))} |
| | </CommandGroup> |
| | </CommandList> |
| | </Command> |
| | </PopoverContent> |
| | </Popover> |
| | ) |
| | } |