canina/frontend/application/components/Tooltip.tsx
parsa aghaei 517d611b2a
Some checks failed
Deploy Canina / deploy (push) Successful in 2m13s
E2E Playwright Tests / Run Full E2E & Security Suites (push) Failing after 6s
fix(ui): ensure 100% solid white background and higher z-index for ingredient wiki tooltip
2026-09-02 10:45:00 +03:30

130 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Info, ExternalLink } from 'lucide-react';
import { SCIENTIFIC_TERMS } from '../lib/data/scientificTerms';
import { useSettingsStore } from '../lib/store/settingsStore';
interface TooltipProps {
termKey: string;
children: React.ReactNode;
onWikiNavigate?: (id: string) => void;
}
export default function Tooltip({ termKey, children, onWikiNavigate }: TooltipProps) {
const [isVisible, setIsVisible] = useState(false);
const [positionClass, setPositionClass] = useState("left-1/2 -translate-x-1/2");
const [arrowClass, setArrowClass] = useState("left-1/2 -translate-x-1/2");
const containerRef = useRef<HTMLSpanElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const scientificTerms = useSettingsStore(state => state.scientificTerms);
const termData = scientificTerms[termKey] || SCIENTIFIC_TERMS[termKey];
// Adjust horizontal position dynamically to prevent overflowing viewport edges
useEffect(() => {
if (isVisible && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const screenWidth = window.innerWidth;
if (rect.left < 140) {
// Too close to left edge (in LTR) or screen boundary
setPositionClass("left-0 translate-x-0");
setArrowClass("left-6 translate-x-0");
} else if (screenWidth - rect.right < 140) {
// Too close to right edge
setPositionClass("right-0 translate-x-0");
setArrowClass("right-6 translate-x-0");
} else {
setPositionClass("left-1/2 -translate-x-1/2");
setArrowClass("left-1/2 -translate-x-1/2");
}
}
}, [isVisible]);
// Click outside listener to dismiss on mobile
useEffect(() => {
if (!isVisible) return;
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsVisible(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("touchstart", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("touchstart", handleClickOutside);
};
}, [isVisible]);
if (!termData) return <>{children}</>;
const handleToggle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsVisible(prev => !prev);
};
return (
<span
ref={containerRef}
className={`relative inline-block group cursor-pointer border-b border-dotted border-canina-blue/60 select-none ${isVisible ? 'z-40' : 'z-10'}`}
onClick={handleToggle}
onMouseEnter={() => {
if (typeof window !== "undefined" && window.matchMedia?.("(hover: hover)")?.matches !== false) {
setIsVisible(true);
}
}}
onMouseLeave={() => {
if (typeof window !== "undefined" && window.matchMedia?.("(hover: hover)")?.matches !== false) {
setIsVisible(false);
}
}}
>
{children}
<AnimatePresence>
{isVisible && (
<motion.div
ref={tooltipRef}
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
onClick={(e) => e.stopPropagation()}
style={{ backgroundColor: '#ffffff' }}
className={`absolute bottom-full mb-3 w-72 max-w-[calc(100vw-2rem)] p-4 sm:p-5 !bg-white text-medical-gray-900 rounded-2xl shadow-2xl border-2 border-medical-gray-200 z-[100] text-sm ${positionClass}`}
dir="rtl"
>
<div className="flex items-center gap-2 mb-2 text-canina-blue">
<Info className="w-4 h-4 shrink-0 text-canina-blue" />
<span className="font-black text-xs-plus uppercase tracking-widest text-canina-blue">دانشنامه علمی کنینا</span>
</div>
<h5 className="font-black text-base text-medical-gray-900 mb-2 leading-snug">{termData.term}</h5>
<p className="text-medical-gray-800 text-xs leading-relaxed mb-4 font-bold text-justify">
{termData.definition}
</p>
{termData.wikiId && (
<button
onClick={(e) => {
e.stopPropagation();
onWikiNavigate?.(termData.wikiId!);
}}
className="flex items-center justify-between text-canina-blue hover:text-blue-700 transition-colors text-xs font-black cursor-pointer pt-2 border-t border-medical-gray-100 w-full"
>
<span>مطالعه مقاله کامل</span>
<ExternalLink className="w-3.5 h-3.5 shrink-0" />
</button>
)}
{/* Arrow */}
<div
style={{ borderTopColor: '#ffffff' }}
className={`absolute top-full border-8 border-transparent border-t-white ${arrowClass}`}
/>
</motion.div>
)}
</AnimatePresence>
</span>
);
}