mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
fix(ui): hand tooltips over between adjacent triggers (#2365)
This commit is contained in:
@@ -34,6 +34,7 @@ import {AnimatePresence, motion} from 'framer-motion';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React, {useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState} from 'react';
|
||||
import {useExclusiveTooltip} from './TooltipExclusivity';
|
||||
import {isTooltipHandoffWarm, markTooltipOpen} from './TooltipHandoff';
|
||||
import {getTooltipScrollSuppressRemainingMs, subscribeTooltipScrollHide} from './TooltipScrollCoordinator';
|
||||
|
||||
const logger = new Logger('Tooltip');
|
||||
@@ -466,9 +467,10 @@ export const Tooltip = observer(
|
||||
}, [updatePositionNow, cancelRaf, getTooltipOwnerWindow]);
|
||||
const getVisibilityDriverDelayMs = useCallback(() => {
|
||||
const scrollSuppressRemainingMs = getTooltipScrollSuppressRemainingMs();
|
||||
const openDelay = isTooltipHandoffWarm() ? 0 : delay;
|
||||
switch (true) {
|
||||
case delay > 0 || scrollSuppressRemainingMs > 0:
|
||||
return Math.max(delay, scrollSuppressRemainingMs > 0 ? scrollSuppressRemainingMs + 1 : 0);
|
||||
case openDelay > 0 || scrollSuppressRemainingMs > 0:
|
||||
return Math.max(openDelay, scrollSuppressRemainingMs > 0 ? scrollSuppressRemainingMs + 1 : 0);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@@ -602,6 +604,10 @@ export const Tooltip = observer(
|
||||
cancelRaf();
|
||||
};
|
||||
}, [cancelRaf, clearDelayTimer, clearPointerFocusTimeout]);
|
||||
useEffect(() => {
|
||||
if (!shouldRenderTooltip) return;
|
||||
return markTooltipOpen();
|
||||
}, [shouldRenderTooltip]);
|
||||
useEffect(() => {
|
||||
if (!shouldRenderTooltip) return;
|
||||
return subscribeTooltipScrollHide(dismissTooltip);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const TOOLTIP_HANDOFF_GRACE_MS = 300;
|
||||
|
||||
let openCount = 0;
|
||||
let warmUntil = 0;
|
||||
let graceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function now(): number {
|
||||
return typeof performance === 'undefined' ? Date.now() : performance.now();
|
||||
}
|
||||
|
||||
function clearGraceTimer(): void {
|
||||
if (graceTimer == null) return;
|
||||
clearTimeout(graceTimer);
|
||||
graceTimer = null;
|
||||
}
|
||||
|
||||
function armGraceTimer(): void {
|
||||
clearGraceTimer();
|
||||
const remaining = warmUntil - now();
|
||||
if (remaining <= 0) return;
|
||||
graceTimer = setTimeout(() => {
|
||||
graceTimer = null;
|
||||
warmUntil = 0;
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
export function markTooltipOpen(): () => void {
|
||||
openCount += 1;
|
||||
warmUntil = 0;
|
||||
clearGraceTimer();
|
||||
let released = false;
|
||||
return () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
openCount = Math.max(0, openCount - 1);
|
||||
if (openCount === 0) {
|
||||
warmUntil = now() + TOOLTIP_HANDOFF_GRACE_MS;
|
||||
armGraceTimer();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isTooltipHandoffWarm(): boolean {
|
||||
return openCount > 0 || now() < warmUntil;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import type React from 'react';
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useExclusiveTooltip} from './TooltipExclusivity';
|
||||
import {isTooltipHandoffWarm, markTooltipOpen} from './TooltipHandoff';
|
||||
import {subscribeTooltipScrollHide} from './TooltipScrollCoordinator';
|
||||
|
||||
export interface HoverFloatingTooltipState {
|
||||
@@ -67,7 +68,10 @@ export function useHoverFloatingTooltip(hoverDelay = 500, placement: Placement =
|
||||
middleware,
|
||||
whileElementsMounted: hoverFloatingAutoUpdate,
|
||||
});
|
||||
const hoverDelayConfig = useMemo(() => ({open: hoverDelay, close: CLOSE_DELAY_MS}), [hoverDelay]);
|
||||
const hoverDelayConfig = useCallback(
|
||||
() => ({open: isTooltipHandoffWarm() ? 0 : hoverDelay, close: CLOSE_DELAY_MS}),
|
||||
[hoverDelay],
|
||||
);
|
||||
const hoverSafePolygon = useMemo(() => safePolygon({buffer: SAFE_POLYGON_BUFFER_PX, requireIntent: false}), []);
|
||||
const hover = useHover(context, {
|
||||
delay: hoverDelayConfig,
|
||||
@@ -95,6 +99,10 @@ export function useHoverFloatingTooltip(hoverDelay = 500, placement: Placement =
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
useExclusiveTooltip(isOpen, hide);
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
return markTooltipOpen();
|
||||
}, [isOpen]);
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
return subscribeTooltipScrollHide(hide);
|
||||
|
||||
Reference in New Issue
Block a user