import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import CheckmarkIcon from '../assets/checkmark.svg'; import NoFilesDarkIcon from '../assets/no-files-dark.svg'; import NoFilesIcon from '../assets/no-files.svg'; import { useDarkTheme } from '../hooks'; import Input from './Input'; export type OptionType = { id: string | number; label: string; icon?: string | React.ReactNode; [key: string]: any; }; type MultiSelectPopupProps = { isOpen: boolean; onClose: () => void; anchorRef: React.RefObject; options: OptionType[]; selectedIds: Set; onSelectionChange: (newSelectedIds: Set) => void; title?: string; searchPlaceholder?: string; noOptionsMessage?: string; loading?: boolean; footerContent?: React.ReactNode; showSearch?: boolean; singleSelect?: boolean; }; export default function MultiSelectPopup({ isOpen, onClose, anchorRef, options, selectedIds, onSelectionChange, title, searchPlaceholder, noOptionsMessage, loading = false, footerContent, showSearch = true, singleSelect = false, }: MultiSelectPopupProps) { const { t } = useTranslation(); const [isDarkTheme] = useDarkTheme(); const popupRef = useRef(null); const [searchTerm, setSearchTerm] = useState(''); const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0, maxHeight: 0, showAbove: false, }); const filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchTerm.toLowerCase()), ); const handleOptionClick = (optionId: string | number) => { let newSelectedIds: Set; if (singleSelect) newSelectedIds = new Set(); else newSelectedIds = new Set(selectedIds); if (newSelectedIds.has(optionId)) { newSelectedIds.delete(optionId); } else newSelectedIds.add(optionId); onSelectionChange(newSelectedIds); }; const renderIcon = (icon: string | React.ReactNode) => { if (typeof icon === 'string') { if (icon.startsWith('/') || icon.startsWith('http')) { return ( ); } return ( ); } return {icon}; }; useLayoutEffect(() => { if (!isOpen || !anchorRef.current) return; const updatePosition = () => { if (!anchorRef.current) return; const rect = anchorRef.current.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; const popupPadding = 16; const popupMinWidth = 300; const popupMaxWidth = 462; const popupDefaultHeight = 300; const spaceAbove = rect.top; const spaceBelow = viewportHeight - rect.bottom; const showAbove = spaceBelow < popupDefaultHeight && spaceAbove >= popupDefaultHeight; const maxHeight = Math.max( 150, showAbove ? spaceAbove - popupPadding : spaceBelow - popupPadding, ); const availableWidth = viewportWidth - 20; const calculatedWidth = Math.min(popupMaxWidth, availableWidth); let left = rect.left; if (left + calculatedWidth > viewportWidth - 10) { left = viewportWidth - calculatedWidth - 10; } left = Math.max(10, left); setPopupPosition({ top: showAbove ? rect.top - 8 : rect.bottom + 8, left: left, maxHeight: Math.min(600, maxHeight), showAbove, }); }; updatePosition(); window.addEventListener('resize', updatePosition); window.addEventListener('scroll', updatePosition, true); return () => { window.removeEventListener('resize', updatePosition); window.removeEventListener('scroll', updatePosition, true); }; }, [isOpen, anchorRef]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( popupRef.current && !popupRef.current.contains(event.target as Node) && anchorRef.current && !anchorRef.current.contains(event.target as Node) ) onClose(); }; if (isOpen) document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [onClose, anchorRef, isOpen]); useEffect(() => { if (!isOpen) setSearchTerm(''); }, [isOpen]); if (!isOpen) return null; return (
{(title || showSearch) && (
{title && (

{title}

)} {showSearch && ( setSearchTerm(e.target.value)} placeholder={ searchPlaceholder || t('settings.tools.searchPlaceholder', 'Search...') } labelBgClassName="bg-lotion dark:bg-charleston-green-2" borderVariant="thin" className="mb-4" textSize="small" /> )}
)}
{loading ? (
) : (
{filteredOptions.length === 0 ? (
No options found

{searchTerm ? 'No results found' : noOptionsMessage || 'No options available'}

) : ( filteredOptions.map((option) => { const isSelected = selectedIds.has(option.id); return (
handleOptionClick(option.id)} className="dark:border-dim-gray dark:hover:bg-charleston-green-3 flex cursor-pointer items-center justify-between border-b border-[#D9D9D9] p-3 last:border-b-0 hover:bg-gray-100" role="option" aria-selected={isSelected} >
{option.icon && renderIcon(option.icon)}

{option.label}

); }) )}
)}
{footerContent && (
{footerContent}
)}
); }