import React, { useRef, cloneElement, useEffect, useMemo, useState, ReactElement, } from "react"; import classnames from "classnames"; import { bem } from "../../utilities/bem"; import { useKeyPress } from "../../hooks"; import { Box } from "../Box"; import { Popover, PopoverTooltipSharedProps } from "../Popover"; import { KEY_CODE } from "../../types"; import { TooltipContext, useTooltipContext } from "./context"; import { tokens } from "../../style-dictionary/dist/tokens"; export interface TooltipProps extends PopoverTooltipSharedProps { /** * Number of milliseconds to delay the disclosure of the tooltip */ delay?: | number | { show?: number; hide?: number; }; /** * Controls the background and text color */ theme?: "light" | "dark"; /** * Controls the visibility of the reference pointer arrow */ hideArrow?: boolean; /** * Controlled state */ open?: boolean; /** * Controlled state */ setOpen?(open: boolean): void; } const DEFAULT_DELAY = 200; const cn = "Tooltip"; type ReferenceRenderFunction = (props: { isOpen: boolean }) => ReactElement; export const TooltipReference = React.forwardRef< HTMLDivElement, { children: ReferenceRenderFunction | ReactElement; } >(({ children, ...props }, ref) => { const { isOpen } = useTooltipContext(); return ( {typeof children === "function" ? children({ isOpen }) : children} ); }); export const Tooltip = (props: TooltipProps) => { const { isVisible: isVisibleInitial = false, open: isOpenControlled, setOpen: setOpenControlled, children, referenceElement, placement = "bottom", className, delay, theme = "dark", hideArrow, style, ...rest } = props; const [isOpenUncontrolled, setOpenUncontrolled] = useState(isVisibleInitial); const open = isOpenControlled ?? isOpenUncontrolled; const setOpen = setOpenControlled ?? setOpenUncontrolled; const isHoveringReferenceRef = useRef(false); const isHoveringPopoverRef = useRef(false); const referenceTimoutRef = useRef(null); const popoverTimoutRef = useRef(null); const showDelay = useMemo(() => { if (typeof delay === "number") { return delay; } if (typeof delay !== "undefined" && typeof delay.show === "number") { return delay.show; } return DEFAULT_DELAY; }, [delay]); const hideDelay = useMemo(() => { if (typeof delay === "number") { return delay; } if (typeof delay !== "undefined" && typeof delay.hide === "number") { return delay.hide; } return DEFAULT_DELAY; }, [delay]); useEffect(() => { if (isVisibleInitial) { setOpen(true); } }, [isVisibleInitial, setOpen]); useEffect(() => { return () => { if (referenceTimoutRef.current) { clearTimeout(referenceTimoutRef.current); } if (popoverTimoutRef.current) { clearTimeout(popoverTimoutRef.current); } }; }); const close = () => { if (!isHoveringPopoverRef.current && !isHoveringReferenceRef.current) { setOpen(false); } }; const handleMouseEnter = () => { isHoveringReferenceRef.current = true; if (!open) { const referenceTimeout = setTimeout(() => { if (isHoveringReferenceRef.current) { setOpen(true); } }, showDelay); referenceTimoutRef.current = referenceTimeout; } }; const handleMouseLeave = () => { isHoveringReferenceRef.current = false; if (open) { const popoverTimeout = setTimeout(close, hideDelay); popoverTimoutRef.current = popoverTimeout; } }; const handlePopoverMouseEnter = () => { if (!isHoveringPopoverRef.current) { isHoveringPopoverRef.current = true; } }; const handlePopoverMouseLeave = () => { isHoveringPopoverRef.current = false; if (open) { const popoverTimeout = setTimeout(close, hideDelay); popoverTimoutRef.current = popoverTimeout; } }; useKeyPress(KEY_CODE.ESC, () => { if (open && !isVisibleInitial) { close(); } }); const referenceElementWithMouseHandlers = !isVisibleInitial ? cloneElement(referenceElement, { onMouseEnter: () => { handleMouseEnter(); if (typeof referenceElement.props.onMouseEnter === "function") { referenceElement.props.onMouseEnter(); } }, onMouseLeave: () => { handleMouseLeave(); if (typeof referenceElement.props.onMouseLeave === "function") { referenceElement.props.onMouseLeave(); } }, onFocus: () => { handleMouseEnter(); if (typeof referenceElement.props.onFocus === "function") { referenceElement.props.onFocus(); } }, onBlur: () => { handleMouseLeave(); if (typeof referenceElement.props.onBlur === "function") { referenceElement.props.onBlur(); } }, tabIndex: referenceElement.props.tabIndex || 0, }) : referenceElement; const context = useMemo( () => ({ isOpen: open, }), [open], ); return children ? ( {children} ) : ( referenceElement ); };