import React, { ReactElement, useState, useContext, useEffect, useRef, } from 'react'; import { usePopper } from 'react-popper'; import { ThemeContext } from 'styled-components'; import css from '../../utils/css'; import { TooltipContainer, TooltipWrapper, Arrow } from './StyledTooltip'; import { CommonProps } from '../common'; export interface TooltipProps extends CommonProps { /** * Content of tooltip. */ content: string | ReactElement; /** * Whether the tooltip content can be interactive, allowing you to hover over and click inside it. */ interactive?: boolean; /** * Milliseconds for interaction timeout, this only takes effect when `interactive` is enabled. */ interactiveTimeout?: number; /** * Position of tooltip. */ placement?: 'top' | 'bottom' | 'right' | 'left'; /** The target where tooltip is relatively placed to. */ target: string | ReactElement; /** Whether or not to show Tooltip when hovering the target. */ visible?: boolean; } const FALLBACK_PLACEMENT_MAP: { bottom: 'top'; left: 'right'; right: 'left'; top: 'bottom'; } = { bottom: 'top', top: 'bottom', left: 'right', right: 'left', }; const Tooltip = ({ content, placement = 'top', target, interactive = false, interactiveTimeout = 200, id, className, style, sx = {}, 'data-test-id': dataTestId, visible = true, }: TooltipProps): ReactElement => { const [ containerElement, setContainerElement, ] = useState(null); const [tooltipElement, setTooltipElement] = useState( null ); const [arrowElement, setArrowElement] = useState(null); const [open, setOpen] = useState(false); const mouseEnteredContentRef = useRef(false); const interactiveTimerId = useRef(); const theme = useContext(ThemeContext); const { styles, attributes } = usePopper(containerElement, tooltipElement, { strategy: 'fixed', placement, modifiers: [ { name: 'arrow', options: { element: arrowElement }, }, { name: 'offset', options: { offset: [0, theme.space.tooltip.margin], }, }, { name: 'flip', options: { fallbackPlacements: [FALLBACK_PLACEMENT_MAP[placement]], }, }, { name: 'computeStyles', options: { adaptive: false, gpuAcceleration: false, }, }, ], }); const delayCloseTooltipContent = React.useCallback(() => { const closeTooltip = (): void => setOpen(false); if (interactive === true) { interactiveTimerId.current = window.setTimeout(() => { if (mouseEnteredContentRef.current === false) { closeTooltip(); } }, interactiveTimeout); } else { interactiveTimerId.current = undefined; closeTooltip(); } }, [interactive, interactiveTimeout]); useEffect(() => { if (tooltipElement !== null) { tooltipElement.addEventListener('mouseleave', () => { mouseEnteredContentRef.current = false; }); tooltipElement.addEventListener('mouseenter', () => { mouseEnteredContentRef.current = true; }); } }, [tooltipElement]); useEffect(() => { containerElement?.addEventListener('mouseleave', delayCloseTooltipContent); return () => { containerElement?.removeEventListener( 'mouseleave', delayCloseTooltipContent ); }; }, [containerElement, delayCloseTooltipContent]); useEffect(() => { return () => window.clearTimeout(interactiveTimerId.current); }, []); return ( setOpen(true)} onTouchStart={(): void => setOpen(true)} id={id} className={className} style={{ ...style, ...css(sx) }} data-test-id={dataTestId} > {target} {open === true && visible === true && ( {content} )} ); }; export default Tooltip;