import classNames from 'classnames'; import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { Manager, Popper, Reference } from 'react-popper'; import Portal from 'Components/Portal'; import { kinds, tooltipPositions } from 'Helpers/Props'; import { Kind } from 'Helpers/Props/kinds'; import dimensions from 'Styles/Variables/dimensions'; import { isMobile as isMobileUtil } from 'Utilities/browser'; import styles from './Tooltip.css'; export interface TooltipProps { className?: string; bodyClassName?: string; anchor: React.ReactNode; tooltip: string | React.ReactNode; kind?: Extract; position?: (typeof tooltipPositions.all)[number]; canFlip?: boolean; } function Tooltip(props: TooltipProps) { const { className, bodyClassName = styles.body, anchor, tooltip, kind = kinds.DEFAULT, position = tooltipPositions.TOP, canFlip = false, } = props; const closeTimeout = useRef(0); const updater = useRef<(() => void) | null>(null); const [isOpen, setIsOpen] = useState(false); const handleClick = useCallback(() => { if (!isMobileUtil()) { return; } setIsOpen((isOpen) => { return !isOpen; }); }, [setIsOpen]); const handleMouseEnter = useCallback(() => { // Mobile will fire mouse enter and click events rapidly, // this causes the tooltip not to open on the first press. // Ignore the mouse enter event on mobile. if (isMobileUtil()) { return; } if (closeTimeout.current) { window.clearTimeout(closeTimeout.current); } setIsOpen(true); }, [setIsOpen]); const handleMouseLeave = useCallback(() => { // Still listen for mouse leave on mobile to allow clicks outside to close the tooltip. setTimeout(() => { setIsOpen(false); }, 100); }, [setIsOpen]); const maxWidth = useMemo(() => { const windowWidth = window.innerWidth; if (windowWidth >= parseInt(dimensions.breakpointLarge)) { return 800; } else if (windowWidth >= parseInt(dimensions.breakpointMedium)) { return 650; } else if (windowWidth >= parseInt(dimensions.breakpointSmall)) { return 500; } return 450; }, []); const computeMaxSize = useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any (data: any) => { const { top, right, bottom, left } = data.offsets.reference; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; if (/^top/.test(data.placement)) { data.styles.maxHeight = top - 20; } else if (/^bottom/.test(data.placement)) { data.styles.maxHeight = windowHeight - bottom - 20; } else if (/^right/.test(data.placement)) { data.styles.maxWidth = Math.min(maxWidth, windowWidth - right - 20); data.styles.maxHeight = top - 20; } else { data.styles.maxWidth = Math.min(maxWidth, left - 20); data.styles.maxHeight = top - 20; } return data; }, [maxWidth] ); useEffect(() => { const currentTimeout = closeTimeout.current; if (updater.current && isOpen) { updater.current(); } return () => { if (currentTimeout) { window.clearTimeout(currentTimeout); } }; }); return ( {({ ref }) => ( {anchor} )} {({ ref, style, placement, arrowProps, scheduleUpdate }) => { updater.current = scheduleUpdate; const popperPlacement = placement ? placement.split('-')[0] : position; const vertical = popperPlacement === 'top' || popperPlacement === 'bottom'; return (
{isOpen ? (
{tooltip}
) : null}
); }} ); } export default Tooltip;