import type { ReactNode } from 'react' import * as React from 'react' const PopoverContext = React.createContext({ open: false, onOpenChange: (_open: boolean) => {}, }) type PopoverProps = { children?: ReactNode open?: boolean onOpenChange?: (open: boolean) => void } type PopoverTriggerProps = React.HTMLAttributes & { children?: ReactNode nativeButton?: boolean render?: React.ReactElement } type PopoverContentProps = React.HTMLAttributes & { children?: ReactNode placement?: string sideOffset?: number alignOffset?: number popupClassName?: string positionerProps?: React.HTMLAttributes popupProps?: React.HTMLAttributes } export const Popover = ({ children, open, onOpenChange, }: PopoverProps) => { const [localOpen, setLocalOpen] = React.useState(false) const resolvedOpen = open ?? localOpen const handleOpenChange = React.useCallback((nextOpen: boolean) => { setLocalOpen(nextOpen) onOpenChange?.(nextOpen) }, [onOpenChange]) React.useEffect(() => { if (!resolvedOpen) return const handleMouseDown = (event: MouseEvent) => { const target = event.target as Element | null if (target?.closest?.('[data-popover-trigger="true"], [data-popover-content="true"]')) return handleOpenChange(false) } const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') handleOpenChange(false) } document.addEventListener('mousedown', handleMouseDown) document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('mousedown', handleMouseDown) document.removeEventListener('keydown', handleKeyDown) } }, [resolvedOpen, handleOpenChange]) return (
{children}
) } export const PopoverTrigger = ({ children, render, nativeButton: _nativeButton, onClick, ...props }: PopoverTriggerProps) => { const { open, onOpenChange } = React.useContext(PopoverContext) const node = render ?? children if (React.isValidElement(node)) { const triggerElement = node as React.ReactElement> const childProps = (triggerElement.props ?? {}) as React.HTMLAttributes & { 'data-testid'?: string } const triggerProps = props as React.HTMLAttributes & { 'data-testid'?: string } return React.cloneElement(triggerElement, { ...props, ...childProps, 'data-testid': childProps['data-testid'] ?? triggerProps['data-testid'] ?? 'popover-trigger', 'data-popover-trigger': 'true', 'onClick': (event: React.MouseEvent) => { childProps.onClick?.(event) onClick?.(event) if (event.defaultPrevented) return onOpenChange(!open) }, }, render ? (children ?? childProps.children) : childProps.children) } return (
{ onClick?.(event) if (event.defaultPrevented) return onOpenChange(!open) }} {...props} > {node}
) } export const PopoverContent = ({ children, className, placement, sideOffset, alignOffset, popupClassName, positionerProps, popupProps, ...props }: PopoverContentProps) => { const { open } = React.useContext(PopoverContext) if (!open) return null return (
{children}
) } export const PopoverClose = ({ children }: { children?: ReactNode }) => <>{children} export const PopoverTitle = ({ children }: { children?: ReactNode }) => <>{children} export const PopoverDescription = ({ children }: { children?: ReactNode }) => <>{children}