Skip to content

Commit f42a1ad

Browse files
committed
Fix tooltip (3)
1 parent 9c00fff commit f42a1ad

1 file changed

Lines changed: 28 additions & 7 deletions

File tree

web/components/widgets/tooltip.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,36 @@ export function Tooltip(props: {
4040

4141
const [open, setOpen] = useState(false)
4242

43-
// --- Mobile tap suppression to prevent accidental neighbor tooltips ---
44-
// After a tap/click on touch devices, browsers may emit hover/focus events on elements
45-
// that appear under the finger due to layout shifts (e.g., when a card is removed).
43+
// --- Mobile/tap guards to prevent accidental neighbor tooltips ---
44+
// After a tap/click on touch devices, browsers may emit hover/focus-like events
45+
// on elements that appear under the finger due to layout shifts (e.g., when a card is removed).
4646
// We keep a short global cooldown window during which tooltip "open" requests are ignored.
47-
// This helps avoid ghost tooltips appearing on adjacent cards after actions like hide/remove.
47+
// Additionally, we default hover behavior to mouse-only on touch-capable devices.
4848
const nowFn = () => Date.now()
49+
const SUPPRESS_MS = 900
4950
// Module-level shared state
5051
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5152
const g: any = (globalThis as any)
5253
if (g.__tooltipLastTapTs === undefined) g.__tooltipLastTapTs = 0 as number
5354
if (g.__tooltipListenersSetup === undefined) g.__tooltipListenersSetup = false as boolean
5455

56+
const isTouchCapable = () => {
57+
if (typeof window === 'undefined') return false
58+
// Prefer pointer hints
59+
// noinspection JSUnresolvedReference
60+
const nav: any = navigator as any
61+
const hasMP = !!nav && typeof nav.maxTouchPoints === 'number' && nav.maxTouchPoints > 0
62+
const hasTP = !!nav && typeof nav.msMaxTouchPoints === 'number' && nav.msMaxTouchPoints > 0
63+
const mm = typeof window.matchMedia === 'function'
64+
? window.matchMedia('(hover: none) and (pointer: coarse)')
65+
: null
66+
const mm2 = typeof window.matchMedia === 'function'
67+
? window.matchMedia('(any-hover: none)')
68+
: null
69+
const hasOntouch = 'ontouchstart' in window
70+
return hasMP || hasTP || !!mm?.matches || !!mm2?.matches || hasOntouch
71+
}
72+
5573
useEffect(() => {
5674
if (g.__tooltipListenersSetup) return
5775
if (typeof window === 'undefined') return
@@ -60,9 +78,11 @@ export function Tooltip(props: {
6078
}
6179
// Mark taps/pointerdowns (especially touch) globally
6280
window.addEventListener('touchstart', markTap, {passive: true})
81+
window.addEventListener('touchend', markTap, {passive: true})
6382
window.addEventListener('pointerdown', markTap, {passive: true})
64-
// Fallback for some browsers
83+
// Fallbacks for some browsers
6584
window.addEventListener('mousedown', markTap, {passive: true})
85+
window.addEventListener('click', markTap, {passive: true})
6686
g.__tooltipListenersSetup = true
6787
return () => {
6888
// We intentionally do not remove listeners to avoid duplicating across many instances.
@@ -86,7 +106,7 @@ export function Tooltip(props: {
86106
if (next) {
87107
const dt = nowFn() - g.__tooltipLastTapTs
88108
// Ignore open requests shortly after a tap/click (mobile gesture)
89-
if (dt >= 0 && dt < 400) return
109+
if (dt >= 0 && dt < SUPPRESS_MS) return
90110
}
91111
setOpen(next)
92112
},
@@ -104,7 +124,8 @@ export function Tooltip(props: {
104124

105125
const { getReferenceProps, getFloatingProps } = useInteractions([
106126
useHover(context, {
107-
mouseOnly: noTap,
127+
// On touch-capable devices, default to mouse-only hover unless explicitly overridden via noTap=false
128+
mouseOnly: noTap ?? isTouchCapable(),
108129
handleClose: hasSafePolygon ? safePolygon({ buffer: -0.5 }) : null,
109130
}),
110131
useRole(context, { role: 'tooltip' }),

0 commit comments

Comments
 (0)