import React, { useState, ReactElement, useEffect, useRef, useImperativeHandle } from "react"; import ReactDOM from "react-dom"; import cn from "classnames"; import { Modifiers } from "popper.js"; import { Manager, Reference, Popper, PopperProps } from "react-popper"; import PopoverBody from "./popover-body"; export interface IPopoverProps { actions?: React.MutableRefObject<{ close?: () => void; open?: () => void; }>; avoidPreRender?: boolean; className?: string | object; content?: ReactElement | ReactElement[] | string; flexibleContent?: boolean; modifiers?: Modifiers; onToggle?: (isShow: boolean) => void; placement: PopperProps["placement"]; reference: ReactElement; usePortal?: boolean; ["data-testid"]?: string; } export interface IPopoverWrapperProps { scheduleUpdate: () => void; } type IUseClickOutsideHandlerResult = [ boolean, React.Dispatch>, React.MutableRefObject, React.MutableRefObject ]; function usePopoverContainer() { const portalContainerRef = useRef(); useEffect(() => { let portalContainer = document.querySelector("#honeyui-popover-container") as HTMLDivElement; if (!portalContainer) { portalContainer = document.createElement("div"); portalContainer.setAttribute("id", "honeyui-popover-container"); document.body.appendChild(portalContainer); } portalContainerRef.current = portalContainer; }, []); return portalContainerRef; } function useClickOutsideHandler(): IUseClickOutsideHandlerResult { const [isShow, setIsShow] = useState(false); const referenceRef = useRef(null); const popoverRef = useRef(null); const refs = [referenceRef, popoverRef]; useEffect(() => { if (!isShow) { return; } function handleClickOutside({ target }: MouseEvent) { if (!refs.find(ref => ref.current && ref.current.contains(target as Node))) { setIsShow(false); } } const clickOptions = { capture: true }; document.addEventListener("click", handleClickOutside, clickOptions); return () => { document.removeEventListener("click", handleClickOutside, clickOptions); }; }, [isShow]); return [isShow, setIsShow, referenceRef, popoverRef]; } const PopoverWrapper: React.FC = props => { const { scheduleUpdate, children } = props; const mounted = useRef(false); useEffect(() => { if (!mounted.current) { mounted.current = true; } else { scheduleUpdate(); } }, [children]); return <>{children}; }; const Popover: React.FC = props => { const { actions, avoidPreRender, children, className, content, flexibleContent, modifiers, onToggle, placement, reference, usePortal } = props; const [isShow, setIsShow, referenceRef, popoverRef] = useClickOutsideHandler(); const testId = props["data-testid"] || "honeyui-popover"; const portalContainerRef = usePopoverContainer(); useEffect(() => { if (onToggle) { onToggle(isShow); } }, [isShow]); // This creates actions object parent can execute useImperativeHandle(actions, () => ({ close: () => setIsShow(false), open: () => setIsShow(true) })); const renderPopper = () => ( (popoverRef.current = node)} > {({ ref, style, arrowProps, placement: actualPlacement, scheduleUpdate }) => { return (
`bs-popover-${p}`) : null, { fade: !isShow, show: isShow, visible: isShow, invisible: !isShow }, className )} role="tooltip" >
{content ? {content} : children}
); }} ); return ( (referenceRef.current = node)}> {({ ref }) => { return React.cloneElement(React.Children.only(reference), { ref, onClick: (event: React.SyntheticEvent) => { event.preventDefault(); event.stopPropagation(); setIsShow(!isShow); } }); }} {/* eslint-disable @typescript-eslint/indent */} {usePortal ? isShow && portalContainerRef.current ? ReactDOM.createPortal(renderPopper(), portalContainerRef.current) : null : avoidPreRender ? isShow && renderPopper() : renderPopper()} {/* eslint-enable */} ); }; Popover.displayName = "Popover"; Popover.defaultProps = { flexibleContent: false, usePortal: false }; export default Popover;