"use client"; import { ChevronDown, XCircle, XIcon } from "lucide-react"; import { type ReactNode, useEffect, useId, useRef, useState } from "react"; import { Badge } from "@/components/shadcn/badge/badge"; import { Button } from "@/components/shadcn/button/button"; import { Checkbox } from "@/components/shadcn/checkbox/checkbox"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/shadcn/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/shadcn/popover"; import { Separator } from "@/components/shadcn/separator/separator"; import { cn } from "@/lib/utils"; interface MultiSelectOption { label: string; value: string; icon?: ReactNode; description?: string; disabled?: boolean; } interface EnhancedMultiSelectProps { options: MultiSelectOption[]; onValueChange: (values: string[]) => void; defaultValue?: string[]; placeholder?: string; searchable?: boolean; hideSelectAll?: boolean; maxCount?: number; closeOnSelect?: boolean; resetOnDefaultValueChange?: boolean; emptyIndicator?: ReactNode; disabled?: boolean; className?: string; id?: string; "aria-label"?: string; } function arraysEqual(a: string[], b: string[]): boolean { if (a.length !== b.length) return false; const sortedA = [...a].sort(); const sortedB = [...b].sort(); return sortedA.every((val, index) => val === sortedB[index]); } export function EnhancedMultiSelect({ options, onValueChange, defaultValue = [], placeholder = "Select options", searchable = true, hideSelectAll = false, maxCount = 3, closeOnSelect = false, resetOnDefaultValueChange = true, emptyIndicator, disabled = false, className, id, "aria-label": ariaLabel, }: EnhancedMultiSelectProps) { const [selectedValues, setSelectedValues] = useState(defaultValue); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [portalContainer, setPortalContainer] = useState( null, ); const buttonRef = useRef(null); const prevDefaultValueRef = useRef(defaultValue); const selectedAtOpenRef = useRef(selectedValues); const multiSelectId = useId(); const listboxId = `${multiSelectId}-listbox`; // Detect dialog container for portal stacking (critical for Jira modal) useEffect(() => { if (!buttonRef.current) return; const closestDialogContainer = buttonRef.current.closest( "[data-slot='dialog-content'], [data-slot='modal-content'], [role='dialog']", ); setPortalContainer( closestDialogContainer instanceof HTMLElement ? closestDialogContainer : null, ); }, []); // Reset when defaultValue changes externally (e.g. React Hook Form reset) useEffect(() => { if (!resetOnDefaultValueChange) return; const prev = prevDefaultValueRef.current; if (!arraysEqual(prev, defaultValue)) { if (!arraysEqual(selectedValues, defaultValue)) { setSelectedValues(defaultValue); } prevDefaultValueRef.current = [...defaultValue]; } }, [defaultValue, selectedValues, resetOnDefaultValueChange]); function handleOpenChange(nextOpen: boolean) { if (nextOpen) { selectedAtOpenRef.current = [...selectedValues]; } else { setSearch(""); } setOpen(nextOpen); } const enabledOptions = options.filter((o) => !o.disabled); const filteredOptions = ( searchable && search ? options.filter( (o) => o.label.toLowerCase().includes(search.toLowerCase()) || o.value.toLowerCase().includes(search.toLowerCase()), ) : options ).toSorted((a, b) => { const snapshot = selectedAtOpenRef.current; const aSelected = snapshot.includes(a.value) ? 0 : 1; const bSelected = snapshot.includes(b.value) ? 0 : 1; return aSelected - bSelected; }); function getOptionByValue(value: string) { return options.find((o) => o.value === value); } function toggleOption(value: string) { if (disabled) return; const option = getOptionByValue(value); if (option?.disabled) return; const next = selectedValues.includes(value) ? selectedValues.filter((v) => v !== value) : [...selectedValues, value]; setSelectedValues(next); onValueChange(next); if (closeOnSelect) setOpen(false); } function toggleAll() { if (disabled) return; if (selectedValues.length === enabledOptions.length) { handleClear(); } else { const all = enabledOptions.map((o) => o.value); setSelectedValues(all); onValueChange(all); } if (closeOnSelect) setOpen(false); } function handleClear() { if (disabled) return; setSelectedValues([]); onValueChange([]); } return ( setOpen(false)} > {searchable && ( )} {emptyIndicator || "No results found."} {!hideSelectAll && !search && ( )} {filteredOptions.map((option) => { const isSelected = selectedValues.includes(option.value); return ( toggleOption(option.value)} role="option" aria-selected={isSelected} aria-disabled={option.disabled} className={cn( "cursor-pointer", option.disabled && "cursor-not-allowed opacity-50", )} disabled={option.disabled} > ); })}
{selectedValues.length > 0 && ( <> )}
); } EnhancedMultiSelect.displayName = "EnhancedMultiSelect"; export type { EnhancedMultiSelectProps, MultiSelectOption };