import { useState, useEffect, useRef } from "react"; import { Input } from "@/src/components/ui/input"; import { Button } from "@/src/components/ui/button"; import { Search, X, MoreHorizontal } from "lucide-react"; interface MultiSelectComboboxProps { selectedItems: T[]; onItemsChange: (items: T[]) => void; searchQuery: string; onSearchChange: (query: string) => void; searchResults: T[]; isLoading?: boolean; placeholder?: string; hasMoreResults?: boolean; renderItem: ( item: T, isSelected: boolean, onToggle: () => void, ) => React.ReactNode; renderSelectedItem: (item: T, onRemove: () => void) => React.ReactNode; getItemKey: (item: T) => string; disabled?: boolean; onOpenChange?: (open: boolean) => void; } export function MultiSelectCombobox({ selectedItems, onItemsChange, searchQuery, onSearchChange, searchResults, isLoading = false, placeholder = "Search...", hasMoreResults = false, renderItem, renderSelectedItem, getItemKey, disabled = false, onOpenChange, }: MultiSelectComboboxProps) { const [isInputFocused, setIsInputFocused] = useState(false); const [showDropdown, setShowDropdown] = useState(false); const [previousResults, setPreviousResults] = useState([]); const inputRef = useRef(null); const containerRef = useRef(null); const dropdownRef = useRef(null); // Handle focus/blur for dropdown visibility const handleInputFocus = () => { setIsInputFocused(true); setShowDropdown(true); onOpenChange?.(true); }; const handleInputBlur = () => { setIsInputFocused(false); // Delay hiding dropdown to allow clicking on dropdown items setTimeout(() => { if (!isInputFocused) { setShowDropdown(false); onOpenChange?.(false); } }, 200); }; // Auto-scroll to input when items are added/removed useEffect(() => { if (inputRef.current && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [selectedItems.length]); // Update previous results when new data arrives (not loading) useEffect(() => { if (!isLoading && searchResults.length > 0) { setPreviousResults(searchResults); } }, [isLoading, searchResults]); // Handle click outside to close dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && containerRef.current && !dropdownRef.current.contains(event.target as Node) && !containerRef.current.contains(event.target as Node) ) { setShowDropdown(false); setIsInputFocused(false); onOpenChange?.(false); } }; if (showDropdown) { document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; } }, [showDropdown, onOpenChange]); const handleItemToggle = (item: T) => { const itemKey = getItemKey(item); const isSelected = selectedItems.some( (selected) => getItemKey(selected) === itemKey, ); if (isSelected) { onItemsChange( selectedItems.filter((selected) => getItemKey(selected) !== itemKey), ); } else { onItemsChange([...selectedItems, item]); } // Keep dropdown open after item toggle setShowDropdown(true); setIsInputFocused(true); }; const handleItemRemove = (item: T) => { const itemKey = getItemKey(item); onItemsChange( selectedItems.filter((selected) => getItemKey(selected) !== itemKey), ); }; return (
{/* Custom Input with Embedded Pills */}
{/* Selected Items Pills */} {selectedItems.map((item) => (
{renderSelectedItem(item, () => handleItemRemove(item))}
))} {/* Search Input */} onSearchChange(e.target.value)} onFocus={handleInputFocus} onBlur={handleInputBlur} disabled={disabled} className="min-w-24 flex-1 border-none bg-transparent text-xs outline-none placeholder:text-muted-foreground" />
{searchQuery && ( )}
{/* Search Results Dropdown */} {showDropdown && (
{searchResults.length > 0 || (isLoading && previousResults.length > 0) ? (
e.preventDefault()} > {(isLoading && previousResults.length > 0 ? previousResults : searchResults ).map((item, index, array) => (
{renderItem( item, selectedItems.some( (selected) => getItemKey(selected) === getItemKey(item), ), () => handleItemToggle(item), )} {(index < array.length - 1 || hasMoreResults) && (
)}
))} {hasMoreResults && (

More results available, refine your search

)}
) : (
{searchQuery ? `No results found for "${searchQuery}"` : "No results available"}
)}
)}
); }