import * as React from "react"; import FocusLock from "react-focus-lock"; import { css, cx } from "@emotion/css"; import { Dropdownable } from "../../dropdownable"; import PopoverBox from "./PopoverBox"; import { SpacingBox } from "../../styleUtils/modifiers"; import { DropdownableProps, Direction } from "../../dropdownable/components/Dropdownable"; import { ResetButton } from "../../button"; import { display } from "../../shared/styles/styleUtils"; export interface PopoverProps extends Omit { /** * A unique identifier to help screenreaders or UI testing tools associate the dropdown content with it's trigger button. If an ID prop is not provided, an arbitrary ID will be generated. */ id?: string; /** * Whether the dropdown starts open */ initialIsOpen?: boolean; /** * The maximum height of the dropdown overlay, represented by number of pixels. */ maxHeight?: number; /** * The maximum width of the dropdown overlay, represented by number of pixels */ maxWidth?: number; /** * The node that opens the menu when clicked */ trigger: React.ReactNode; /** * Human-readable selector used for writing tests */ "data-cy"?: string; /** * Allows custom styling */ className?: string; } const Popover = ({ children, className, "data-cy": dataCy = "popover", id, initialIsOpen, maxHeight, maxWidth, overlayRoot, preferredDirections, trigger }: PopoverProps) => { const generatedDropdownId = `dropdown-${React.useId()}`; const containerRef = React.useRef(null); const triggerRef = React.useRef(null); const [isOpen, setIsOpen] = React.useState(Boolean(initialIsOpen)); const handleTriggerBlur = () => { setTimeout(() => { if (containerRef?.current?.contains(document.activeElement)) { return; } setIsOpen(false); }, 0); }; const handleClickOutside = e => { if ( containerRef?.current?.contains(e.target) || triggerRef.current?.contains(e.target) || triggerRef.current === e.target ) { return; } setIsOpen(false); }; const handleEscKey = e => { if (e.key === "Escape") { setIsOpen(false); } }; const handleTriggerKeyDown = e => { if (["ArrowDown", " ", "Enter"].includes(e.key)) { // prevents page from scrolling when using down arrow or spacebar e.preventDefault(); setIsOpen(true); } }; const sharedTriggerProps = { onClick: () => { setIsOpen(!isOpen); }, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur, "aria-owns": id || generatedDropdownId, "aria-expanded": isOpen }; React.useEffect(() => { if (isOpen) { document.addEventListener("mouseup", handleClickOutside); document.addEventListener("keyup", handleEscKey); } else { document.removeEventListener("mouseup", handleClickOutside); document.removeEventListener("keyup", handleEscKey); } return () => { document.removeEventListener("mouseup", handleClickOutside); document.removeEventListener("keyup", handleEscKey); }; }, [isOpen]); return (
{children}
} overlayRoot={overlayRoot} > {React.isValidElement(trigger) ? ( React.cloneElement(trigger as React.ReactElement, { ...sharedTriggerProps, tabIndex: 0 }) ) : ( {trigger} )}
); }; export default Popover;