Fix info tooltip partially hidden and gone too quickly on mobile
This commit is contained in:
@@ -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>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<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>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<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>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<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>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
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')}
|
||||
>
|
||||
<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>
|
||||
|
||||
95
src/components/ui/info-tooltip.tsx
Normal file
95
src/components/ui/info-tooltip.tsx
Normal 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];
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user