import { cloneElement, isValidElement, useCallback, useLayoutEffect, type MouseEvent, type ReactElement, type ReactNode, type Ref, } from 'react'; import { cx } from '../utils/cx'; import { composeRefs } from '../utils/refs'; import type { Align, Placement } from '../utils/types'; import { useControllableState } from '../hooks/useControllableState'; import { useDismiss } from '../hooks/useDismiss'; import { Anchor } from './internal/Anchor'; import { useFloatingSurface } from './internal/useFloatingSurface'; import { Portal } from './Portal'; export interface TriggerInjectedProps { ref?: Ref; onClick?: (event: MouseEvent) => void; onKeyDown?: (event: React.KeyboardEvent) => void; 'aria-expanded'?: boolean; 'aria-haspopup'?: 'dialog' | 'menu' | 'listbox' | 'true'; 'aria-controls'?: string; } export interface PopoverProps { trigger: ReactElement; children: ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; placement?: Placement; align?: Align; offset?: number; ariaLabel?: string; padding?: 'none' | 'md'; className?: string; matchWidth?: boolean; role?: 'dialog' | 'listbox'; onKeyDown?: (event: React.KeyboardEvent) => void; } export function Popover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', align = 'center', offset = 8, ariaLabel, padding = 'md', className, matchWidth = false, role = 'dialog', onKeyDown, }: PopoverProps) { const [isOpen, setOpen] = useControllableState(open, defaultOpen, onOpenChange); const { anchorRef, triggerRef, panelRef, panelId, mounted, state, position, refs } = useFloatingSurface({ open: isOpen, placement, align, offset, matchWidth, idSuffix: 'popover', }); const dismiss = useCallback(() => setOpen(false), [setOpen]); useDismiss({ enabled: isOpen, onDismiss: dismiss, refs }); useLayoutEffect(() => { if (role === 'listbox' && isOpen && mounted) panelRef.current?.focus(); }, [role, isOpen, mounted, panelRef]); const triggerEl = trigger as ReactElement; const triggerElement = isValidElement(trigger) ? cloneElement(triggerEl, { ref: composeRefs(triggerRef, triggerEl.props.ref), 'aria-expanded': isOpen, 'aria-haspopup': role === 'listbox' ? 'listbox' : 'dialog', 'aria-controls': mounted ? panelId : undefined, onClick: (event: MouseEvent) => { triggerEl.props.onClick?.(event); setOpen(!isOpen); }, }) : trigger; return ( <> {triggerElement} {mounted && (
{children}
)} ); }