import React, { type ReactNode, useRef } from 'react'; import { classNames, getStyleFromPlacement, type Placement } from '../../utils'; import { useId } from '../../hooks/useId'; import { useWindowEvent } from '../../hooks/useWindowEvent'; import { useAnimationState } from '../../hooks/useAnimationState'; import styles from './Popover.module.css'; import commonStyles from '../../theme/common.module.css'; export type TriggerProps = { 'aria-expanded': boolean; 'aria-controls': string; 'aria-haspopup': 'dialog'; id: string; isDropdown: boolean; onClick: () => void; }; export type PopoverProps = { /** * The contents of the Popover */ children: (closeDropdown: () => void) => ReactNode; /** * Function to render the button trigger */ renderTrigger: (triggerProps: TriggerProps, isOpen: boolean) => ReactNode; /** * className for the element */ className?: string; /** * Whether the Popover should be align at the start or end of the trigger * @default bottom-start */ placement?: Placement; }; export const Popover = ({ children, renderTrigger, className, placement = 'bottom-start', }: PopoverProps) => { const rootRef = useRef(null); const dialogId = useId(); const buttonId = useId(); const [isVisible, isClosing, , closePopover, togglePopover] = useAnimationState('close'); useWindowEvent('mousedown', (event) => { const { current: el } = rootRef; if (el && el.contains(event.target as Node)) { return; } closePopover(); }); useWindowEvent( 'focus', () => { const { current: el } = rootRef; if (el && el.contains(document.activeElement)) { return; } closePopover(); }, true, ); return (
{renderTrigger( { 'aria-expanded': isVisible, 'aria-controls': dialogId, 'aria-haspopup': 'dialog', id: buttonId, isDropdown: true, onClick: togglePopover, }, isVisible, )} {isVisible && ( )}
); };