import clsx from "clsx"; import { motion } from "motion/react"; import React, { useEffect, useRef, useState } from "react"; interface TooltipProps { label: string; children: React.ReactNode; position?: "top" | "bottom" | "left" | "right" | "cursor"; showDelay?: number; className?: string; } interface TooltipPosition { top?: number | string; left?: number | string; bottom?: number | string; right?: number | string; transform?: string; } export default function Tooltip({ label, children, position = "top", showDelay = 500, className, }: TooltipProps) { const [isVisible, setIsVisible] = useState(false); const [tooltipPosition, setTooltipPosition] = useState({}); const tooltipWrapperRef = useRef(null); const tooltipRef = useRef(null); useEffect(() => { if (!tooltipWrapperRef.current) return; const current = tooltipWrapperRef.current; current.addEventListener("mouseenter", () => setIsVisible(true)); current.addEventListener("mouseleave", () => setIsVisible(false)); return () => { current?.removeEventListener("mouseenter", () => setIsVisible(true)); current?.removeEventListener("mouseleave", () => setIsVisible(false)); }; }, []); useEffect(() => { if (!isVisible || !tooltipRef.current || !tooltipWrapperRef.current) return; const updatePosition = () => { const pos = calculatePosition(position, tooltipRef, tooltipWrapperRef); setTooltipPosition(pos); }; const handleScroll = () => { setIsVisible(false); }; updatePosition(); // Скрываем tooltip при скролле, обновляем позицию при ресайзе window.addEventListener("scroll", handleScroll, true); window.addEventListener("resize", updatePosition); return () => { window.removeEventListener("scroll", handleScroll, true); window.removeEventListener("resize", updatePosition); }; }, [isVisible, position]); return (
{children} {isVisible && ( } /> )}
); } function TooltipContent({ label, position, ref, }: { label: string; position: TooltipPosition; ref: React.RefObject; }) { return (
{label}
); } function calculatePosition( position: TooltipProps["position"], tooltipRef: React.RefObject, tooltipWrapperRef: React.RefObject ) { if (!tooltipWrapperRef.current || !tooltipRef.current) return {}; const wrapperRect = tooltipWrapperRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); const GAP = 8; function adjustHorizontal(baseLeft: number) { let left = baseLeft; let transform = "translateX(-50%)"; const tooltipLeft = left - tooltipRect.width / 2; const tooltipRight = tooltipLeft + tooltipRect.width; // Если не влезает горизонтально, то сдвигаем if (tooltipLeft < GAP) { left = GAP + tooltipRect.width / 2; transform = "translateX(-50%)"; } else if (tooltipRight > window.innerWidth - GAP) { left = window.innerWidth - GAP - tooltipRect.width / 2; transform = "translateX(-50%)"; } return { left, transform }; } function adjustVertical(baseTop: number) { return { top: baseTop, transform: "translateY(-50%)" }; } switch (position) { case "top": { const top = wrapperRect.top - tooltipRect.height - GAP; const left = wrapperRect.left + wrapperRect.width / 2; // Если не влезает сверху, показываем снизу if (top < GAP) { const bottomTop = wrapperRect.bottom + GAP; return { top: bottomTop, ...adjustHorizontal(left) }; } return { top, ...adjustHorizontal(left) }; } case "bottom": { const top = wrapperRect.bottom + GAP; const left = wrapperRect.left + wrapperRect.width / 2; // Если не влезает снизу, показываем сверху if (top + tooltipRect.height > window.innerHeight - GAP) { const topTop = wrapperRect.top - tooltipRect.height - GAP; return { top: topTop, ...adjustHorizontal(left) }; } return { top, ...adjustHorizontal(left) }; } case "left": { const left = wrapperRect.left - tooltipRect.width - GAP; const top = wrapperRect.top + wrapperRect.height / 2; // Если не влезает слева, показываем справа if (left < GAP) { const rightLeft = wrapperRect.right + GAP; return { left: rightLeft, ...adjustVertical(top) }; } return { left, ...adjustVertical(top) }; } case "right": { const left = wrapperRect.right + GAP; const top = wrapperRect.top + wrapperRect.height / 2; // Если не влезает справа, показываем слева if (left + tooltipRect.width > window.innerWidth - GAP) { const leftLeft = wrapperRect.left - tooltipRect.width - GAP; return { left: leftLeft, ...adjustVertical(top) }; } return { left, ...adjustVertical(top) }; } case "cursor": { return { transform: "none" }; } } return {}; }