Files
stream.graff.tech-new/client/src/components/ui/Tooltip.tsx
T
mikhail_lanskikh 28091d732a Enhance UI components and functionality across the application
- Updated ActionsSidebarWrapper to accept a ref for improved positioning.
- Enhanced SessionUsersPanel with new props for participant management, including mute and disable video functionalities.
- Added vertical positioning option to ActionsPopover for better alignment.
- Modified QRCodePopup and SharePopup to include a second argument in setPopup for type differentiation.
- Refactored ControlButton and Tooltip components for improved accessibility and styling.
- Updated UserCamera to integrate ActionsPopover for participant controls, enhancing user interaction.
- Improved PopoverWrapper to handle dynamic positioning based on parent element.
- Adjusted UserDevicesControls for better layout consistency and responsiveness.
- Enhanced popup management in the popupStore to track popup types.
2025-11-06 17:15:30 +05:00

198 lines
6.0 KiB
TypeScript

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<TooltipPosition>({});
const tooltipWrapperRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(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 (
<div ref={tooltipWrapperRef} className={clsx("cursor-pointer", className)}>
{children}
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: showDelay / 1000, duration: 0.3 }}
>
<TooltipContent
label={label}
position={tooltipPosition || {}}
ref={tooltipRef as React.RefObject<HTMLDivElement>}
/>
</motion.div>
)}
</div>
);
}
function TooltipContent({
label,
position,
ref,
}: {
label: string;
position: TooltipPosition;
ref: React.RefObject<HTMLDivElement>;
}) {
return (
<div
ref={ref}
style={{ ...position, transform: position.transform || "none" }}
className="z-[9999] fixed 2xl:px-[0.694vw] 2xl:py-[0.417vw] px-[10px] py-[6px] bg-[#00000073] caption-s text-white 2xl:rounded-[0.556vw] rounded-lg whitespace-nowrap backdrop-blur-[4px] shadow-[0_2px_4px_0_#00000059] cursor-default"
>
{label}
</div>
);
}
function calculatePosition(
position: TooltipProps["position"],
tooltipRef: React.RefObject<HTMLDivElement | null>,
tooltipWrapperRef: React.RefObject<HTMLDivElement | null>
) {
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 {};
}