import { useState, useEffect, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils'; /** * TooltipProps - Propriétés du composant Tooltip * * @interface TooltipProps */ export interface TooltipProps { /** * Contenu du tooltip (texte ou élément React) */ content: React.ReactNode; /** * Élément enfant qui déclenche le tooltip */ children: React.ReactNode; /** * Position du tooltip par rapport à l'élément * * - `top`: Au-dessus * - `bottom`: En-dessous * - `left`: À gauche * - `right`: À droite * * @default 'top' */ position?: 'top' | 'bottom' | 'left' | 'right'; /** * Événement qui déclenche l'affichage du tooltip * * - `hover`: Au survol (par défaut) * - `click`: Au clic * - `focus`: Au focus * * @default 'hover' */ trigger?: 'hover' | 'click' | 'focus'; /** * Délai avant l'affichage en millisecondes * * @default 200 */ delay?: number; /** * Si `true`, affiche une flèche pointant vers l'élément * * @default true */ showArrow?: boolean; /** * Largeur maximale du tooltip en pixels * * @default 300 */ maxWidth?: number; /** * Si `true`, désactive le tooltip * * @default false */ disabled?: boolean; /** * Classes CSS personnalisées */ className?: string; } /** * Tooltip - Composant de tooltip avec design system Kodo * * Composant de tooltip avec : * - Positionnement intelligent (flip et shift automatiques) * - Plusieurs triggers (hover, click, focus) * - Délai configurable * - Flèche optionnelle * - Gestion du viewport * * @example * ```tsx * // Tooltip simple au survol * * * * ``` * * @example * ```tsx * // Tooltip avec position et trigger personnalisés * * * * ``` * * @component * @param {TooltipProps} props - Propriétés du composant * @returns {JSX.Element} Wrapper avec tooltip positionné */ export function Tooltip({ content, children, position = 'top', trigger = 'hover', delay = 200, showArrow = true, maxWidth = 300, disabled = false, className, }: TooltipProps) { const [visible, setVisible] = useState(false); const [isMounted, setIsMounted] = useState(false); const [calculatedPosition, setCalculatedPosition] = useState(position); const [tooltipStyle, setTooltipStyle] = useState({}); const timeoutRef = useRef(null); const hideTimeoutRef = useRef(null); const wrapperRef = useRef(null); const tooltipRef = useRef(null); // Calcul du positionnement avec flip et shift const calculatePosition = useCallback(() => { if (!wrapperRef.current || !tooltipRef.current || !visible) return; const wrapperRect = wrapperRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const margin = 8; let newPosition = position; let offsetX = 0; let offsetY = 0; // Flip: inverser la position si le tooltip sort de l'écran switch (position) { case 'top': if (wrapperRect.top - tooltipRect.height - margin < 0) { newPosition = 'bottom'; } break; case 'bottom': if (wrapperRect.bottom + tooltipRect.height + margin > viewportHeight) { newPosition = 'top'; } break; case 'left': if (wrapperRect.left - tooltipRect.width - margin < 0) { newPosition = 'right'; } break; case 'right': if (wrapperRect.right + tooltipRect.width + margin > viewportWidth) { newPosition = 'left'; } break; } // Shift: ajuster la position pour rester dans la viewport if (newPosition === 'top' || newPosition === 'bottom') { const centerX = wrapperRect.left + wrapperRect.width / 2; const tooltipHalfWidth = tooltipRect.width / 2; const minX = margin; const maxX = viewportWidth - margin; if (centerX - tooltipHalfWidth < minX) { offsetX = minX - (centerX - tooltipHalfWidth); } else if (centerX + tooltipHalfWidth > maxX) { offsetX = maxX - (centerX + tooltipHalfWidth); } } else { const centerY = wrapperRect.top + wrapperRect.height / 2; const tooltipHalfHeight = tooltipRect.height / 2; const minY = margin; const maxY = viewportHeight - margin; if (centerY - tooltipHalfHeight < minY) { offsetY = minY - (centerY - tooltipHalfHeight); } else if (centerY + tooltipHalfHeight > maxY) { offsetY = maxY - (centerY + tooltipHalfHeight); } } setCalculatedPosition(newPosition); setTooltipStyle({ ...(offsetX !== 0 && { transform: `translate(calc(-50% + ${offsetX}px), 0)`, }), ...(offsetY !== 0 && { transform: `translate(0, calc(-50% + ${offsetY}px))`, }), }); }, [position, visible]); useEffect(() => { if (visible) { setIsMounted(true); calculatePosition(); } }, [visible, calculatePosition]); useEffect(() => { if (isMounted && visible) { calculatePosition(); } }, [isMounted, visible, calculatePosition]); const showTooltip = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { setVisible(true); }, delay); }; const hideTooltip = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } hideTimeoutRef.current = setTimeout(() => { setVisible(false); }, 100); }; const handleClick = () => { if (trigger === 'click') { setVisible(!visible); } }; const triggerProps = { hover: { onMouseEnter: showTooltip, onMouseLeave: hideTooltip, }, click: { onClick: handleClick, }, focus: { onFocus: showTooltip, onBlur: hideTooltip, }, }[trigger]; const positionClasses = { top: 'bottom-full left-1/2 -translate-x-1/2 mb-2', bottom: 'top-full left-1/2 -translate-x-1/2 mt-2', left: 'right-full top-1/2 -translate-y-1/2 mr-2', right: 'left-full top-1/2 -translate-y-1/2 ml-2', }; const arrowClasses = { top: 'top-full left-1/2 -translate-x-1/2 border-t-kodo-ink border-l-transparent border-r-transparent border-b-transparent', bottom: 'bottom-full left-1/2 -translate-x-1/2 border-b-kodo-ink border-l-transparent border-r-transparent border-t-transparent', left: 'left-full top-1/2 -translate-y-1/2 border-l-kodo-ink border-t-transparent border-b-transparent border-r-transparent', right: 'right-full top-1/2 -translate-y-1/2 border-r-kodo-ink border-t-transparent border-b-transparent border-l-transparent', }; useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } }; }, []); if (disabled) { return <>{children}; } const tooltipContent = ( <> {isMounted && (
{content} {showArrow && (
)}
)} ); return (
{children} {tooltipContent}
); }