Files
thrilltrack-explorer/src-old/components/ui/multi-select-combobox.tsx

144 lines
4.1 KiB
TypeScript

import * as React from "react";
import { useState, useMemo } from "react";
import { Check, ChevronsUpDown, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export interface MultiSelectOption {
label: string;
value: string;
}
interface MultiSelectComboboxProps {
options: MultiSelectOption[];
value?: string[];
onValueChange: (values: string[]) => void;
placeholder?: string;
searchPlaceholder?: string;
emptyText?: string;
maxDisplay?: number;
className?: string;
}
export function MultiSelectCombobox({
options,
value = [],
onValueChange,
placeholder = "Select options...",
searchPlaceholder = "Search...",
emptyText = "No options found.",
maxDisplay = 2,
className,
}: MultiSelectComboboxProps) {
const [open, setOpen] = useState(false);
const handleSelect = (selectedValue: string) => {
const newValues = value.includes(selectedValue)
? value.filter((v) => v !== selectedValue)
: [...value, selectedValue];
onValueChange(newValues);
};
const handleRemove = (valueToRemove: string) => {
onValueChange(value.filter((v) => v !== valueToRemove));
};
const displayText = useMemo(() => {
if (value.length === 0) return placeholder;
if (value.length <= maxDisplay) {
return value
.map((v) => options.find((o) => o.value === v)?.label)
.filter(Boolean)
.join(", ");
}
return `${value.length} selected`;
}, [value, options, maxDisplay, placeholder]);
return (
<div className="space-y-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between", className)}
>
<span className="truncate">{displayText}</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0 z-50" align="start">
<Command>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>{emptyText}</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => handleSelect(option.value)}
>
<Checkbox
checked={value.includes(option.value)}
className="mr-2"
/>
<span>{option.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Selected Items Display */}
{value.length > 0 && (
<div className="flex flex-wrap gap-2">
{value.map((v) => {
const option = options.find((o) => o.value === v);
return option ? (
<Badge key={v} variant="secondary" className="gap-1">
{option.label}
<X
className="w-3 h-3 cursor-pointer hover:text-destructive"
onClick={(e) => {
e.stopPropagation();
handleRemove(v);
}}
/>
</Badge>
) : null;
})}
{value.length > 1 && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => onValueChange([])}
>
Clear all
</Button>
)}
</div>
)}
</div>
);
}