import React, { useEffect, useRef, useState } from 'react'; import { clsx } from 'clsx'; export interface PopoverProps { trigger: React.ReactNode; content: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void; align?: 'start' | 'center' | 'end'; side?: 'top' | 'bottom' | 'left' | 'right'; className?: string; triggerClassName?: string; modal?: boolean; } const alignClasses = { start: 'start-0', center: 'left-1/2 -translate-x-1/2', end: 'end-0', }; const sideClasses = { top: 'bottom-full mb-2', bottom: 'top-full mt-2', left: 'right-full mr-2', right: 'left-full ml-2', }; /** * Popover component - contextual popup (different from Tooltip) */ export const Popover = ({ trigger, content, open: controlledOpen, onOpenChange, align = 'center', side = 'bottom', className, triggerClassName, modal = false, }: PopoverProps) => { const [internalOpen, setInternalOpen] = useState(false); const popoverRef = useRef(null); const triggerRef = useRef(null); const isControlled = controlledOpen !== undefined; const open = isControlled ? controlledOpen : internalOpen; const setOpen = (value: boolean) => { if (!isControlled) { setInternalOpen(value); } onOpenChange?.(value); }; useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( popoverRef.current && triggerRef.current && !popoverRef.current.contains(event.target as Node) && !triggerRef.current.contains(event.target as Node) ) { setOpen(false); } }; const handleEscape = (event: KeyboardEvent) => { if (event.key === 'Escape' && open) { setOpen(false); } }; if (open) { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleEscape); } return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscape); }; }, [open]); return (
setOpen(!open)} className={clsx('cursor-pointer', triggerClassName)} role="button" aria-haspopup="true" aria-expanded={open} > {trigger}
{open && ( <> {modal && (
setOpen(false)} aria-hidden="true" /> )}
{content}
)}
); };