import type React from "react"; import { useState, useEffect, useRef } from "react"; interface Option { value: string; text: string; } interface MultiSelectProps { label: string; options: Option[]; defaultSelected?: string[]; value?: string[]; onChange?: (selected: string[]) => void; disabled?: boolean; placeholder?: string; } const MultiSelect: React.FC = ({ label, options, defaultSelected = [], value, onChange, disabled = false, placeholder = "Select options", }) => { const isControlled = value !== undefined; const [internalSelected, setInternalSelected] = useState(defaultSelected); const selectedOptions = isControlled ? value : internalSelected; const [isOpen, setIsOpen] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); const dropdownRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); } }, [isOpen]); const updateSelection = (newSelected: string[]) => { if (!isControlled) setInternalSelected(newSelected); onChange?.(newSelected); }; const toggleDropdown = () => { if (!disabled) { setIsOpen((prev) => !prev); setFocusedIndex(-1); } }; const handleSelect = (optionValue: string) => { const newSelected = selectedOptions.includes(optionValue) ? selectedOptions.filter((v) => v !== optionValue) : [...selectedOptions, optionValue]; updateSelection(newSelected); }; const removeOption = (optionValue: string) => { updateSelection(selectedOptions.filter((v) => v !== optionValue)); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (disabled) return; e.preventDefault(); switch (e.key) { case "Enter": if (!isOpen) { setIsOpen(true); } else if (focusedIndex >= 0) { handleSelect(options[focusedIndex].value); } break; case "Escape": setIsOpen(false); break; case "ArrowDown": if (!isOpen) { setIsOpen(true); } else { setFocusedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0)); } break; case "ArrowUp": if (isOpen) { setFocusedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1)); } break; } }; return (
{selectedOptions.length > 0 ? ( selectedOptions.map((value) => { const text = options.find((opt) => opt.value === value)?.text || value; return (
{text}
); }) ) : (
{placeholder}
)}
{isOpen && (
e.stopPropagation()} role="listbox" aria-label={label} > {options.map((option, index) => { const isSelected = selectedOptions.includes(option.value); const isFocused = index === focusedIndex; return (
handleSelect(option.value)} role="option" aria-selected={isSelected} >
{option.text}
); })}
)}
); }; export default MultiSelect;