@@ -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