import { arrow, autoUpdate, flip, offset, Placement, safePolygon, shift, useClick, useFloating, useHover, useInteractions, useRole, } from '@floating-ui/react' import {Transition} from '@headlessui/react' import {ReactNode, useEffect, useRef, useState} from 'react' // See https://floating-ui.com/docs/react-dom export function Tooltip(props: { text: string | false | undefined | null | ReactNode children: ReactNode className?: string placement?: Placement noTap?: boolean noFade?: boolean hasSafePolygon?: boolean suppressHydrationWarning?: boolean testId?: string }) { const { text, children, className, noTap, noFade, hasSafePolygon, suppressHydrationWarning, testId, } = props const arrowRef = useRef(null) const [open, setOpen] = useState(false) const touchOpenRef = useRef(false) // tracks whether open was triggered by touch const {x, y, refs, strategy, context} = useFloating({ open, onOpenChange: setOpen, whileElementsMounted: autoUpdate, placement: props.placement ?? 'top', middleware: [offset(8), flip(), shift({padding: 4}), arrow({element: arrowRef})], }) // Close tooltip when tapping outside on touch devices useEffect(() => { if (!open || !touchOpenRef.current) return const handleOutsideTouch = (e: TouchEvent) => { const ref = refs.reference.current as Element | null const floating = refs.floating.current as Element | null if ( ref && !ref.contains(e.target as Node) && floating && !floating.contains(e.target as Node) ) { setOpen(false) touchOpenRef.current = false } } document.addEventListener('touchstart', handleOutsideTouch, {passive: true}) return () => document.removeEventListener('touchstart', handleOutsideTouch) }, [open, refs.reference, refs.floating]) const {getReferenceProps, getFloatingProps} = useInteractions([ useHover(context, { // Allow hover on all devices; touch devices will also use onTouchStart below mouseOnly: noTap, handleClose: hasSafePolygon ? safePolygon({buffer: -0.5}) : null, }), useClick(context), useRole(context, {role: 'tooltip'}), ]) const handleTouchStart = (e: React.TouchEvent) => { if (noTap) return e.stopPropagation() const next = !open touchOpenRef.current = next setOpen(next) } return text ? ( <> { touchOpenRef.current = false setOpen(true) }} onMouseLeave={() => { if (!touchOpenRef.current) setOpen(false) }} onTouchStart={handleTouchStart} {...getReferenceProps()} > {children} {/* conditionally render tooltip and fade in/out */}
{text}
) : ( <>{children} ) }