improve visibility for tooltips
This commit is contained in:
parent
2a34588074
commit
fd5af1e2fa
2 changed files with 92 additions and 13 deletions
|
@ -51,10 +51,10 @@ export const InfoCard: React.FC<InfoCardProps> = ({
|
||||||
className={`alert ${typeStyles[type]} shadow-sm ${className}`}
|
className={`alert ${typeStyles[type]} shadow-sm ${className}`}
|
||||||
>
|
>
|
||||||
{icon || defaultIcons[type]}
|
{icon || defaultIcons[type]}
|
||||||
<div className="text-sm space-y-2">
|
<div className="text-sm space-y-2 text-white">
|
||||||
<p className="font-medium">{title}</p>
|
<p className="font-medium text-white">{title}</p>
|
||||||
<motion.ul
|
<motion.ul
|
||||||
className="space-y-1 ml-1"
|
className="space-y-1 ml-1 text-white"
|
||||||
variants={listVariants}
|
variants={listVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="show"
|
animate="show"
|
||||||
|
@ -63,9 +63,9 @@ export const InfoCard: React.FC<InfoCardProps> = ({
|
||||||
<motion.li
|
<motion.li
|
||||||
key={index}
|
key={index}
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
className="flex items-start gap-2"
|
className="flex items-start gap-2 text-white"
|
||||||
>
|
>
|
||||||
<span className="text-base leading-6">•</span>
|
<span className="text-base leading-6 text-white">•</span>
|
||||||
<span>{item}</span>
|
<span>{item}</span>
|
||||||
</motion.li>
|
</motion.li>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ interface TooltipProps {
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define a small safety margin (in pixels) to keep tooltip from touching viewport edges
|
||||||
|
const VIEWPORT_MARGIN = 8;
|
||||||
|
|
||||||
const positionStyles = {
|
const positionStyles = {
|
||||||
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
||||||
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
||||||
|
@ -35,10 +38,81 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||||
icon = 'mdi:information',
|
icon = 'mdi:information',
|
||||||
maxWidth = '350px'
|
maxWidth = '350px'
|
||||||
}) => {
|
}) => {
|
||||||
const [isVisible, setIsVisible] = React.useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [currentPosition, setCurrentPosition] = useState(position);
|
||||||
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||||
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isVisible || !tooltipRef.current || !containerRef.current) return;
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
const tooltip = tooltipRef.current!;
|
||||||
|
const container = containerRef.current!;
|
||||||
|
const tooltipRect = tooltip.getBoundingClientRect();
|
||||||
|
const containerRect = container.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Calculate overflow amounts
|
||||||
|
const overflowRight = Math.max(0, tooltipRect.right - (viewportWidth - VIEWPORT_MARGIN));
|
||||||
|
const overflowLeft = Math.max(0, VIEWPORT_MARGIN - tooltipRect.left);
|
||||||
|
const overflowTop = Math.max(0, VIEWPORT_MARGIN - tooltipRect.top);
|
||||||
|
const overflowBottom = Math.max(0, tooltipRect.bottom - (viewportHeight - VIEWPORT_MARGIN));
|
||||||
|
|
||||||
|
// Initialize offset adjustments
|
||||||
|
let xOffset = 0;
|
||||||
|
let yOffset = 0;
|
||||||
|
|
||||||
|
// Determine best position and calculate offsets
|
||||||
|
let newPosition = position;
|
||||||
|
|
||||||
|
if (position === 'left' || position === 'right') {
|
||||||
|
if (position === 'left' && overflowLeft > 0) {
|
||||||
|
newPosition = 'right';
|
||||||
|
} else if (position === 'right' && overflowRight > 0) {
|
||||||
|
newPosition = 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust vertical position if needed
|
||||||
|
if (overflowTop > 0) {
|
||||||
|
yOffset = overflowTop;
|
||||||
|
} else if (overflowBottom > 0) {
|
||||||
|
yOffset = -overflowBottom;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (position === 'top' && overflowTop > 0) {
|
||||||
|
newPosition = 'bottom';
|
||||||
|
} else if (position === 'bottom' && overflowBottom > 0) {
|
||||||
|
newPosition = 'top';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust horizontal position if needed
|
||||||
|
if (overflowRight > 0) {
|
||||||
|
xOffset = -overflowRight;
|
||||||
|
} else if (overflowLeft > 0) {
|
||||||
|
xOffset = overflowLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPosition(newPosition);
|
||||||
|
setOffset({ x: xOffset, y: yOffset });
|
||||||
|
};
|
||||||
|
|
||||||
|
updatePosition();
|
||||||
|
window.addEventListener('resize', updatePosition);
|
||||||
|
window.addEventListener('scroll', updatePosition);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updatePosition);
|
||||||
|
window.removeEventListener('scroll', updatePosition);
|
||||||
|
};
|
||||||
|
}, [isVisible, position]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={`relative inline-block ${className}`}
|
className={`relative inline-block ${className}`}
|
||||||
onMouseEnter={() => setIsVisible(true)}
|
onMouseEnter={() => setIsVisible(true)}
|
||||||
onMouseLeave={() => setIsVisible(false)}
|
onMouseLeave={() => setIsVisible(false)}
|
||||||
|
@ -49,6 +123,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
ref={tooltipRef}
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
@ -56,16 +131,20 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||||
duration: 0.15,
|
duration: 0.15,
|
||||||
ease: 'easeOut'
|
ease: 'easeOut'
|
||||||
}}
|
}}
|
||||||
style={{ maxWidth }}
|
style={{
|
||||||
|
maxWidth,
|
||||||
|
width: 'min(90vw, 350px)',
|
||||||
|
transform: `translate(${offset.x}px, ${offset.y}px)`
|
||||||
|
}}
|
||||||
className={`absolute z-50 p-3 bg-base-200/95 border border-base-300 rounded-lg shadow-lg backdrop-blur-sm
|
className={`absolute z-50 p-3 bg-base-200/95 border border-base-300 rounded-lg shadow-lg backdrop-blur-sm
|
||||||
${positionStyles[position]}`}
|
${positionStyles[currentPosition]}`}
|
||||||
>
|
>
|
||||||
<div className={`absolute w-0 h-0 border-[6px] ${arrowStyles[position]}`} />
|
<div className={`absolute w-0 h-0 border-[6px] ${arrowStyles[currentPosition]}`} />
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
<Icon icon={icon} className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
<Icon icon={icon} className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-medium text-base text-base-content">{title}</h3>
|
<h3 className="font-medium text-base text-base-content break-words">{title}</h3>
|
||||||
<p className="text-sm leading-relaxed text-base-content/80 mt-0.5 whitespace-pre-wrap">{description}</p>
|
<p className="text-sm leading-relaxed text-base-content/80 mt-0.5 whitespace-pre-wrap break-words">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
Loading…
Reference in a new issue