28091d732a
- 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.
198 lines
6.0 KiB
TypeScript
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 {};
|
|
}
|