import classNames from 'classnames'; import type { CSSProperties, KeyboardEvent, ReactNode } from 'react'; import { useRef, useState } from 'react'; import { generateId } from '../../lib/generateId'; import { Portal } from '../Portal'; export interface TooltipProps { children: ReactNode; content: ReactNode; tabIndex?: number; size?: 'md' | 'lg'; className?: string; } const hiddenStyles: CSSProperties = { left: -99999, top: -99999, visibility: 'hidden', pointerEvents: 'none', opacity: 0, }; type TooltipPosition = 'top' | 'bottom'; interface TooltipOpenState { styles: CSSProperties; position: TooltipPosition; } export function Tooltip({ children, className, content, tabIndex, size = 'md' }: TooltipProps) { const [openState, setOpenState] = useState(null); const triggerRef = useRef(null); const tooltipRef = useRef(null); const showTimeout = useRef(undefined); const handleOpenImmediate = () => { if (triggerRef.current == null || tooltipRef.current == null) return; clearTimeout(showTimeout.current); const triggerRect = triggerRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); const viewportHeight = document.documentElement.clientHeight; const margin = 8; const spaceAbove = Math.max(0, triggerRect.top - margin); const spaceBelow = Math.max(0, viewportHeight - triggerRect.bottom - margin); const preferBottom = spaceAbove < tooltipRect.height + margin && spaceBelow > spaceAbove; const position: TooltipPosition = preferBottom ? 'bottom' : 'top'; const styles: CSSProperties = { left: Math.max(0, triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2), maxHeight: position === 'top' ? spaceAbove : spaceBelow, ...(position === 'top' ? { bottom: viewportHeight - triggerRect.top } : { top: triggerRect.bottom }), }; setOpenState({ styles, position }); }; const handleOpen = () => { clearTimeout(showTimeout.current); showTimeout.current = setTimeout(handleOpenImmediate, 500); }; const handleClose = () => { clearTimeout(showTimeout.current); setOpenState(null); }; const handleToggleImmediate = () => { if (openState) handleClose(); else handleOpenImmediate(); }; const handleKeyDown = (e: KeyboardEvent) => { if (openState && e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); handleClose(); } }; const id = useRef(`tooltip-${generateId()}`); return ( <> {/* oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- Needs to be usable in other buttons */} {children} ); } function Triangle({ className, position }: { className?: string; position: 'top' | 'bottom' }) { const isBottom = position === 'bottom'; return ( Triangle ); }