import { ClickAwayListener } from '@mui/base/ClickAwayListener'; import { Popper as PopperUnstyled, PopperProps as PopperUnstyledProps, } from '@mui/base/Popper'; import { styled } from '@mui/system'; import { Box, BoxProps } from '../Box'; import { merge } from 'lodash'; import { cloneElement, JSXElementConstructor, MouseEventHandler, ReactElement, useEffect, useRef, useState, } from 'react'; import { useDevices } from '../hooks'; import { ZIndex } from '../theme/config'; import { useTheme } from '../theme'; const arrowHeight = 8; export interface TooltipProps { title: React.ReactNode | string; maxWidth?: string | number; sx?: BoxProps['sx']; children: ReactElement>; container?: PopperUnstyledProps['container']; direction?: PopperUnstyledProps['direction']; disablePortal?: PopperUnstyledProps['disablePortal']; placement?: PopperUnstyledProps['placement']; popperOptions?: PopperUnstyledProps['popperOptions']; transition?: PopperUnstyledProps['transition']; // @ts-ignore: Unreachable code error componentsProps?: PopperUnstyledProps['componentsProps']; PopperProps?: Partial; onlyHover?: boolean; onlyClick?: boolean; open?: boolean; onOpen?: () => void; onClose?: () => void; enterDelay?: number; leaveDelay?: number; arrow?: boolean; disabled?: boolean; offset?: [number, number]; } export const tooltipClasses = { arrow: 'DODOTooltip-arrow', }; const StyledTooltipRoot = styled('div')( ({ theme }) => ` z-index: ${(theme.zIndex as ZIndex)?.tooltip}; &[data-popper-placement*="bottom"] .${tooltipClasses.arrow} { top: 0; margin-top: -${arrowHeight}px; &::before { transform-origin: 0 100%; } } &[data-popper-placement*="top"] .${tooltipClasses.arrow} { bottom: 0; margin-bottom: -${arrowHeight}px; &::before { transform-origin: 100% 0; } } &[data-popper-placement*="right"] .${tooltipClasses.arrow} { &::before { transform-origin: 100% 100%; } } &[data-popper-placement*="left"] .${tooltipClasses.arrow} { &::before { transform-origin: 0 0; } } `, ); export default function Tooltip({ title, sx, maxWidth = 'none', popperOptions, children, onlyHover, onlyClick, /** This prop won't impact the enter click delay */ enterDelay = 100, /** This prop won't impact the enter click delay */ leaveDelay = 0, arrow = true, PopperProps, open: openProps, onOpen, onClose, disabled, placement = 'top', offset, ...attrs }: TooltipProps) { const theme = useTheme(); const { isMobile } = useDevices(); const enterTooltip = useRef(false); const enterTrigger = useRef(false); const enterTimer = useRef(); const leaveTimer = useRef(); const [childrenRef, setChildrenRef] = useState(); const [arrowRef, setArrowRef] = useState(); const [open, setOpen] = useState(false); const clickEmit = onlyClick || (isMobile && !onlyHover); const handleChangeOpen = (value: boolean) => { if (value) { if (openProps === undefined) { setOpen(true); } if (onOpen) { onOpen(); } } else { if (openProps === undefined) { setOpen(false); } if (onClose) { onClose(); } } }; const handleOverTooltip: MouseEventHandler = () => { if (disabled || clickEmit) return; enterTooltip.current = true; clearTimeout(leaveTimer.current); clearTimeout(enterTimer.current); }; const handleOutTooltip: MouseEventHandler = () => { if (disabled || clickEmit) return; enterTooltip.current = false; clearTimeout(leaveTimer.current); clearTimeout(enterTimer.current); leaveTimer.current = setTimeout(() => { if (!enterTrigger.current && !enterTooltip.current) { handleChangeOpen(false); } }, leaveDelay); }; const childrenProps: any = { ref: setChildrenRef, }; if (!disabled) { if (!clickEmit) { const onMouseEnter = () => { enterTrigger.current = true; clearTimeout(leaveTimer.current); clearTimeout(enterTimer.current); enterTimer.current = setTimeout(() => { handleChangeOpen(true); }, enterDelay); }; const onMouseLeave = () => { enterTrigger.current = false; clearTimeout(leaveTimer.current); clearTimeout(enterTimer.current); leaveTimer.current = setTimeout(() => { if (!enterTrigger.current && !enterTooltip.current) { handleChangeOpen(false); } }, leaveDelay); }; childrenProps.onMouseOut = onMouseEnter; childrenProps.onMouseEnter = onMouseEnter; childrenProps.onMouseLeave = onMouseLeave; } else { childrenProps.onClick = (evt: any) => { evt.stopPropagation(); if (typeof children === 'object' && children.props.onClick) { children.props.onClick(evt); } handleChangeOpen(true); }; } } useEffect(() => { return () => { handleChangeOpen(false); clearTimeout(leaveTimer.current); clearTimeout(enterTimer.current); }; }, []); return ( <> e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()} onTouchEnd={(e) => e.stopPropagation()} > {title} {arrow && ( )} { if (clickEmit) { handleChangeOpen(false); } }} // Avoid conflicts with onClick's stopPropagation mouseEvent="onPointerDown" > {cloneElement(children, childrenProps)} ); }