improve visibility for tooltips

This commit is contained in:
chark1es 2025-02-21 01:38:23 -08:00
parent 2a34588074
commit fd5af1e2fa
2 changed files with 92 additions and 13 deletions

View file

@ -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>
))} ))}

View file

@ -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>