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
);
};