Fix info tooltip partially hidden and gone too quickly on mobile

This commit is contained in:
2026-01-16 18:02:38 +00:00
parent b9e13ae642
commit 0c82519609
3 changed files with 247 additions and 63 deletions

View File

@@ -126,6 +126,27 @@ const Settings = ({
const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true);
const [isAdvancedExpanded, setIsAdvancedExpanded] = React.useState(false);
// Track which tooltip is currently open (for mobile touch interaction)
const [openTooltipId, setOpenTooltipId] = React.useState<string | null>(null);
// Track window width for responsive tooltip positioning
const [isNarrowScreen, setIsNarrowScreen] = React.useState(
typeof window !== 'undefined' ? window.innerWidth < 640 : false
);
// Update narrow screen state on window resize
React.useEffect(() => {
const handleResize = () => {
setIsNarrowScreen(window.innerWidth < 640);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Determine tooltip side based on screen width
const tooltipSide = isNarrowScreen ? 'top' : 'right';
// Load and persist settings card expansion states
React.useEffect(() => {
const savedStates = localStorage.getItem('settingsCardStates_v1');
@@ -142,6 +163,37 @@ const Settings = ({
}
}, []);
// Close tooltip when clicking outside
React.useEffect(() => {
if (!openTooltipId) return;
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
const target = e.target as HTMLElement;
// Check if click is outside tooltip content and button
if (!target.closest('[role="tooltip"]') && !target.closest('button[aria-label*="Tooltip"]')) {
setOpenTooltipId(null);
}
};
// Small delay to prevent immediate closure
setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
}, 10);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, [openTooltipId]);
// Helper to toggle tooltip (for mobile click interaction)
const handleTooltipToggle = (tooltipId: string) => (e: React.MouseEvent<HTMLButtonElement> | React.TouchEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setOpenTooltipId(current => current === tooltipId ? null : tooltipId);
};
const updateDiagramExpanded = (value: boolean) => {
setIsDiagramExpanded(value);
saveCardStates({ diagram: value, simulation: isSimulationExpanded, pharmacokinetic: isPharmacokineticExpanded, advanced: isAdvancedExpanded });
@@ -218,17 +270,19 @@ const Settings = ({
{t('showTemplateDayInChart')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'showTemplateDay'} onOpenChange={(open) => setOpenTooltipId(open ? 'showTemplateDay' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('showTemplateDay')}
onTouchStart={handleTooltipToggle('showTemplateDay')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('showTemplateDayTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTemplateDayTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -247,17 +301,19 @@ const Settings = ({
{t('showDayReferenceLines')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'showDayReferenceLines'} onOpenChange={(open) => setOpenTooltipId(open ? 'showDayReferenceLines' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('showDayReferenceLines')}
onTouchStart={handleTooltipToggle('showDayReferenceLines')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('showDayReferenceLinesTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showDayReferenceLinesTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -276,17 +332,19 @@ const Settings = ({
{t('showTherapeuticRangeLines')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'showTherapeuticRangeLines'} onOpenChange={(open) => setOpenTooltipId(open ? 'showTherapeuticRangeLines' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('showTherapeuticRangeLines')}
onTouchStart={handleTooltipToggle('showTherapeuticRangeLines')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('showTherapeuticRangeLinesTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTherapeuticRangeLinesTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -299,17 +357,19 @@ const Settings = ({
{t('therapeuticRange')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'therapeuticRange'} onOpenChange={(open) => setOpenTooltipId(open ? 'therapeuticRange' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('therapeuticRange')}
onTouchStart={handleTooltipToggle('therapeuticRange')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('therapeuticRangeTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'therapeuticRangeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -347,17 +407,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="font-medium">{t('displayedDays')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'displayedDays'} onOpenChange={(open) => setOpenTooltipId(open ? 'displayedDays' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('displayedDays')}
onTouchStart={handleTooltipToggle('displayedDays')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('displayedDaysTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'displayedDaysTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -379,17 +441,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="font-medium">{t('yAxisRange')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'yAxisRange'} onOpenChange={(open) => setOpenTooltipId(open ? 'yAxisRange' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('yAxisRange')}
onTouchStart={handleTooltipToggle('yAxisRange')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('yAxisRangeTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'yAxisRangeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -438,7 +502,7 @@ const Settings = ({
{t('xAxisFormatContinuous')}
</SelectItem>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs">{t('xAxisFormatContinuousDesc')}</p>
</TooltipContent>
</Tooltip>
@@ -448,7 +512,7 @@ const Settings = ({
{t('xAxisFormat24h')}
</SelectItem>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs">{t('xAxisFormat24hDesc')}</p>
</TooltipContent>
</Tooltip>
@@ -458,7 +522,7 @@ const Settings = ({
{t('xAxisFormat12h')}
</SelectItem>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs">{t('xAxisFormat12hDesc')}</p>
</TooltipContent>
</Tooltip>
@@ -483,17 +547,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="font-medium">{t('simulationDuration')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'simulationDuration'} onOpenChange={(open) => setOpenTooltipId(open ? 'simulationDuration' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('simulationDuration')}
onTouchStart={handleTooltipToggle('simulationDuration')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('simulationDurationTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'simulationDurationTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -533,17 +599,19 @@ const Settings = ({
{t('steadyStateDays')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'steadyStateDays'} onOpenChange={(open) => setOpenTooltipId(open ? 'steadyStateDays' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('steadyStateDays')}
onTouchStart={handleTooltipToggle('steadyStateDays')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('steadyStateDaysTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'steadyStateDaysTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -581,17 +649,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="font-medium">{t('halfLife')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'halfLife'} onOpenChange={(open) => setOpenTooltipId(open ? 'halfLife' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('halfLife')}
onTouchStart={handleTooltipToggle('halfLife')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('halfLifeTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'halfLifeTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
@@ -619,17 +689,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="font-medium">{t('conversionHalfLife')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'conversionHalfLife'} onOpenChange={(open) => setOpenTooltipId(open ? 'conversionHalfLife' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('conversionHalfLife')}
onTouchStart={handleTooltipToggle('conversionHalfLife')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('conversionHalfLifeTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'conversionHalfLifeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -653,17 +725,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="font-medium">{t('absorptionHalfLife')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'absorptionHalfLife'} onOpenChange={(open) => setOpenTooltipId(open ? 'absorptionHalfLife' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('absorptionHalfLife')}
onTouchStart={handleTooltipToggle('absorptionHalfLife')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('absorptionHalfLifeTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'absorptionHalfLifeTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -711,17 +785,19 @@ const Settings = ({
{t('weightBasedVdScaling')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'weightBasedVd'} onOpenChange={(open) => setOpenTooltipId(open ? 'weightBasedVd' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('weightBasedVd')}
onTouchStart={handleTooltipToggle('weightBasedVd')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('weightBasedVdTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'weightBasedVdTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -732,17 +808,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="text-sm font-medium">{t('bodyWeight')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'bodyWeight'} onOpenChange={(open) => setOpenTooltipId(open ? 'bodyWeight' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('bodyWeight')}
onTouchStart={handleTooltipToggle('bodyWeight')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('bodyWeightTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'bodyWeightTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
@@ -775,17 +853,19 @@ const Settings = ({
{t('foodEffectEnabled')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'foodEffect'} onOpenChange={(open) => setOpenTooltipId(open ? 'foodEffect' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('foodEffect')}
onTouchStart={handleTooltipToggle('foodEffect')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('foodEffectTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'foodEffectTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -796,17 +876,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="text-sm font-medium">{t('tmaxDelay')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'tmaxDelay'} onOpenChange={(open) => setOpenTooltipId(open ? 'tmaxDelay' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('tmaxDelay')}
onTouchStart={handleTooltipToggle('tmaxDelay')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('tmaxDelayTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'tmaxDelayTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>
@@ -839,17 +921,19 @@ const Settings = ({
{t('urinePHTendency')}
</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'urinePH'} onOpenChange={(open) => setOpenTooltipId(open ? 'urinePH' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('urinePH')}
onTouchStart={handleTooltipToggle('urinePH')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('urinePHTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'urinePHTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -860,17 +944,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="text-sm font-medium">{t('urinePHValue')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'urinePHValue'} onOpenChange={(open) => setOpenTooltipId(open ? 'urinePHValue' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('urinePHValue')}
onTouchStart={handleTooltipToggle('urinePHValue')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('urinePHValueTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{tWithDefaults(t, 'urinePHValueTooltip', defaultsForT)}</p>
</TooltipContent>
</Tooltip>
@@ -896,17 +982,19 @@ const Settings = ({
<div className="flex items-center gap-2">
<Label className="font-medium">{t('oralBioavailability')}</Label>
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltipId === 'oralBioavailability'} onOpenChange={(open) => setOpenTooltipId(open ? 'oralBioavailability' : null)}>
<TooltipTrigger asChild>
<button
type="button"
onClick={handleTooltipToggle('oralBioavailability')}
onTouchStart={handleTooltipToggle('oralBioavailability')}
className="inline-flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label={t('oralBioavailabilityTooltip')}
>
<Info className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">
<TooltipContent side={tooltipSide}>
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'oralBioavailabilityTooltip', defaultsForT))}</p>
</TooltipContent>
</Tooltip>

View File

@@ -0,0 +1,95 @@
/**
* useInfoTooltip Hook & InfoTooltipButton Component
*
* Provides mobile-friendly tooltip handling for info icons.
* On touch devices, the tooltip persists until user clicks outside.
* On desktop, it shows on hover as normal.
*
* Usage in settings:
* ```tsx
* const [isOpen, handlers] = useInfoTooltip();
* <Tooltip open={isOpen} onOpenChange={setIsOpen}>
* <TooltipTrigger asChild>
* <button {...handlers}>
* <Info className="h-4 w-4" />
* </button>
* </TooltipTrigger>
* <TooltipContent>...</TooltipContent>
* </Tooltip>
* ```
*
* @author Andreas Weyer
* @license MIT
*/
import React from 'react';
interface TooltipHandlers {
onTouchStart: (e: React.TouchEvent<HTMLButtonElement>) => void;
onMouseEnter?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onMouseLeave?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
/**
* Hook to manage tooltip state with touch persistence.
* Returns [isOpen, handlers, setIsOpen] for use with Radix Tooltip.
*/
export const useInfoTooltip = (): [boolean, TooltipHandlers, (open: boolean) => void] => {
const [isOpen, setIsOpen] = React.useState(false);
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
const triggerRef = React.useRef<HTMLButtonElement>(null);
// Detect if device supports touch
React.useEffect(() => {
const isTouchScreen = () => {
return (
(typeof window !== 'undefined' &&
window.matchMedia('(hover: none) and (pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0)
);
};
setIsTouchDevice(isTouchScreen());
}, []);
// Handle click outside to close tooltip (for touch devices)
React.useEffect(() => {
if (!isOpen || !isTouchDevice) return;
const handleClickOutside = (e: MouseEvent | TouchEvent) => {
if (triggerRef.current && !triggerRef.current.contains(e.target as Node)) {
const tooltip = document.querySelector('[role="tooltip"]');
if (tooltip && !tooltip.contains(e.target as Node)) {
setIsOpen(false);
}
}
};
// Use a small delay to avoid immediate closing on the same touch
const timeoutId = setTimeout(() => {
document.addEventListener('touchstart', handleClickOutside);
document.addEventListener('mousedown', handleClickOutside);
}, 100);
return () => {
clearTimeout(timeoutId);
document.removeEventListener('touchstart', handleClickOutside);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, isTouchDevice]);
const handlers: TooltipHandlers = {
onTouchStart: (e: React.TouchEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setIsOpen(true);
},
// For desktop hover, let Radix UI handle it (will work via open prop)
// But we can optionally close on mouse leave for consistency
onMouseLeave: isTouchDevice ? undefined : (e: React.MouseEvent<HTMLButtonElement>) => {
// Let Radix UI handle this naturally
},
};
return [isOpen, handlers, setIsOpen];
};

View File

@@ -5,6 +5,7 @@ import { cn } from "../../lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
// Tooltip with slightly longer delay to support touch interactions better
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger