/** * @file Custom multi-select dropdown component */ import type React from "react"; import { useEffect, useState, useRef } from "react"; interface Option { value: string; label: string; } interface MultiSelectProps { options: Option[]; value?: string[]; defaultSelected?: string[]; onChange?: (selected: string[]) => void; disabled?: boolean; } const MultiSelect: React.FC = ({ options, defaultSelected = [], value, onChange, disabled = false, }) => { const [selectedOptions, setSelectedOptions] = useState(defaultSelected); const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( containerRef.current && !containerRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const toggleDropdown = () => { if (!disabled) setIsOpen((prev) => !prev); }; useEffect(() => { if (selectedOptions.length && value && !value?.length) { onChange?.(defaultSelected); } }, [defaultSelected]); useEffect(() => { if ( value?.length && (value.length !== selectedOptions.length || value.some((val) => !selectedOptions.includes(val))) ) { setSelectedOptions(value); } }, [value, selectedOptions]); const handleSelect = (optionValue: string) => { const newSelectedOptions = selectedOptions.includes(optionValue) ? selectedOptions.filter((value) => value !== optionValue) : [...selectedOptions, optionValue]; setSelectedOptions(newSelectedOptions); onChange?.(newSelectedOptions); }; const removeOption = (value: string) => { const newSelectedOptions = selectedOptions.filter((opt) => opt !== value); setSelectedOptions(newSelectedOptions); onChange?.(newSelectedOptions); }; const selectedValuesText = selectedOptions.map( (value) => options.find((option) => option.value === value)?.label || "" ); return (
{selectedValuesText.length > 0 ? ( selectedValuesText.map((text, index) => (
{text}
{ e.stopPropagation(); removeOption(selectedOptions[index]); }} className="pl-2 text-gray-500 cursor-pointer group-hover:text-gray-400 dark:text-gray-400" >
)) ) : ( )}
{isOpen && (
e.stopPropagation()} >
{options.map((option, index) => (
handleSelect(option.value)} >
{option.label}
))}
)}
); }; export default MultiSelect;