import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { CheckIcon, Cross2Icon, CrossCircledIcon, ChevronDownIcon, } from "@radix-ui/react-icons"; import { cn } from "@/util/utils"; import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/components/ui/command"; /** * Variants for the multi-select component to handle different styles. * Uses class-variance-authority (cva) to define different styles based on "variant" prop. */ const multiSelectVariants = cva("m-1", { variants: { variant: { default: "border-foreground/10 text-foreground bg-card hover:bg-card/80", secondary: "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", inverted: "inverted", }, }, defaultVariants: { variant: "default", }, }); /** * Props for MultiSelect component */ interface MultiSelectProps extends React.ButtonHTMLAttributes, VariantProps { /** * An array of option objects to be displayed in the multi-select component. * Each option object has a label, value, and an optional icon. */ options: { /** The text to display for the option. */ label: string; /** The unique value associated with the option. */ value: string; /** Optional icon component to display alongside the option. */ icon?: React.ComponentType<{ className?: string }>; }[]; /** * Callback function triggered when the selected values change. * Receives an array of the new selected values. */ onValueChange: (value: string[]) => void; /** The default selected values when the component mounts. */ value: string[]; /** * Placeholder text to be displayed when no values are selected. * Optional, defaults to "Select options". */ placeholder?: string; /** * Maximum number of items to display. Extra selected items will be summarized. * Optional, defaults to 3. */ maxCount?: number; /** * The modality of the popover. When set to true, interaction with outside elements * will be disabled and only popover content will be visible to screen readers. * Optional, defaults to false. */ modalPopover?: boolean; /** * Additional class names to apply custom styles to the multi-select component. * Optional, can be used to add custom styles. */ className?: string; } export const MultiSelect = React.forwardRef< HTMLButtonElement, MultiSelectProps >( ( { options, onValueChange, variant, value, placeholder = "Select options", maxCount = 3, modalPopover = false, className, ...props }, ref, ) => { const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const handleInputKeyDown = ( event: React.KeyboardEvent, ) => { if (event.key === "Enter") { setIsPopoverOpen(true); } else if (event.key === "Backspace" && !event.currentTarget.value) { const newSelectedValues = [...value]; newSelectedValues.pop(); onValueChange(newSelectedValues); } }; const toggleOption = (option: string) => { const newSelectedValues = value.includes(option) ? value.filter((v) => v !== option) : [...value, option]; onValueChange(newSelectedValues); }; const handleClear = () => { onValueChange([]); }; const handleTogglePopover = () => { setIsPopoverOpen((prev) => !prev); }; const clearExtraOptions = () => { const newSelectedValues = value.slice(0, maxCount); onValueChange(newSelectedValues); }; const toggleAll = () => { if (value.length === options.length) { handleClear(); } else { const allValues = options.map((option) => option.value); onValueChange(allValues); } }; return ( setIsPopoverOpen(false)} > No results found.
(Select All)
{options.map((option) => { const isSelected = value.includes(option.value); return ( toggleOption(option.value)} className="cursor-pointer" >
{option.icon && ( )} {option.label}
); })}
{value.length > 0 && ( <> Clear )} setIsPopoverOpen(false)} className="max-w-full flex-1 cursor-pointer justify-center" > Close
); }, ); MultiSelect.displayName = "MultiSelect";