import { useState, useRef, useEffect, KeyboardEvent } from 'react'; import PersonIcon from '@mui/icons-material/Person'; import GroupIcon from '@mui/icons-material/Group'; import BusinessIcon from '@mui/icons-material/Business'; import ComputerIcon from '@mui/icons-material/Computer'; import CloudIcon from '@mui/icons-material/Cloud'; import StorageIcon from '@mui/icons-material/Storage'; import DevicesIcon from '@mui/icons-material/Devices'; import AccountTreeIcon from '@mui/icons-material/AccountTree'; import CategoryIcon from '@mui/icons-material/Category'; import LightbulbIcon from '@mui/icons-material/Lightbulb'; import WorkIcon from '@mui/icons-material/Work'; import SchoolIcon from '@mui/icons-material/School'; import LocalHospitalIcon from '@mui/icons-material/LocalHospital'; import AccountBalanceIcon from '@mui/icons-material/AccountBalance'; import StoreIcon from '@mui/icons-material/Store'; import FactoryIcon from '@mui/icons-material/Factory'; import EngineeringIcon from '@mui/icons-material/Engineering'; import ScienceIcon from '@mui/icons-material/Science'; import PublicIcon from '@mui/icons-material/Public'; import LocationCityIcon from '@mui/icons-material/LocationCity'; import CloseIcon from '@mui/icons-material/Close'; /** * IconPopover - Floating popover icon picker * * Features: * - Button trigger with current icon preview * - Floating popover (no nested scrolling) * - Grid display of available icons * - Visual selection feedback * - Keyboard accessible (Escape to close, Arrow keys to navigate) * - Click outside to close * - ARIA compliant */ interface Props { selectedIcon?: string; onSelect: (iconName: string) => void; } // Available icons with their names const availableIcons = [ { name: 'Person', component: PersonIcon }, { name: 'Group', component: GroupIcon }, { name: 'Business', component: BusinessIcon }, { name: 'Computer', component: ComputerIcon }, { name: 'Cloud', component: CloudIcon }, { name: 'Storage', component: StorageIcon }, { name: 'Devices', component: DevicesIcon }, { name: 'AccountTree', component: AccountTreeIcon }, { name: 'Category', component: CategoryIcon }, { name: 'Lightbulb', component: LightbulbIcon }, { name: 'Work', component: WorkIcon }, { name: 'School', component: SchoolIcon }, { name: 'LocalHospital', component: LocalHospitalIcon }, { name: 'AccountBalance', component: AccountBalanceIcon }, { name: 'Store', component: StoreIcon }, { name: 'Factory', component: FactoryIcon }, { name: 'Engineering', component: EngineeringIcon }, { name: 'Science', component: ScienceIcon }, { name: 'Public', component: PublicIcon }, { name: 'LocationCity', component: LocationCityIcon }, ]; const IconPopover = ({ selectedIcon, onSelect }: Props) => { const [isOpen, setIsOpen] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); const buttonRef = useRef(null); const popoverRef = useRef(null); // Find the selected icon component const selectedIconObj = availableIcons.find((icon) => icon.name === selectedIcon); const SelectedIconComponent = selectedIconObj?.component; // Close on click outside useEffect(() => { if (!isOpen) return; const handleClickOutside = (event: MouseEvent) => { if ( popoverRef.current && !popoverRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [isOpen]); // Close on Escape key useEffect(() => { if (!isOpen) return; const handleEscape = (event: globalThis.KeyboardEvent) => { if (event.key === 'Escape') { setIsOpen(false); buttonRef.current?.focus(); } }; document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); }, [isOpen]); const handleSelectIcon = (iconName: string) => { onSelect(iconName); setIsOpen(false); buttonRef.current?.focus(); }; const handleKeyDown = (e: KeyboardEvent, iconName: string, index: number) => { const gridCols = 8; const totalItems = availableIcons.length + 1; // +1 for "no icon" option switch (e.key) { case 'Enter': case ' ': e.preventDefault(); handleSelectIcon(iconName); break; case 'ArrowRight': e.preventDefault(); setFocusedIndex(Math.min(index + 1, totalItems - 1)); break; case 'ArrowLeft': e.preventDefault(); setFocusedIndex(Math.max(index - 1, 0)); break; case 'ArrowDown': e.preventDefault(); setFocusedIndex(Math.min(index + gridCols, totalItems - 1)); break; case 'ArrowUp': e.preventDefault(); setFocusedIndex(Math.max(index - gridCols, 0)); break; } }; // Auto-focus when navigating with keyboard useEffect(() => { if (focusedIndex >= 0 && popoverRef.current) { const buttons = popoverRef.current.querySelectorAll('button'); const button = buttons[focusedIndex] as HTMLButtonElement; button?.focus(); } }, [focusedIndex]); return (
{/* Trigger Button */} {/* Popover */} {isOpen && (
{/* No icon option */} {/* Icon options */} {availableIcons.map((icon, index) => { const IconComponent = icon.component; const actualIndex = index + 1; // +1 because "no icon" is at index 0 return ( ); })}
)}
); }; export default IconPopover;