fix(core): suppress tooltip hover on touch pointer events (#933)

This commit is contained in:
rahim
2026-03-17 23:04:54 -07:00
committed by GitHub
parent 7e0827c330
commit 324ea2fd3b
2 changed files with 119 additions and 3 deletions
@@ -1,5 +1,5 @@
import { flush } from '@videojs/store';
import { describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { TooltipGroupCore } from '../../../../core/ui/tooltip/tooltip-group-core';
import { createTestTooltip } from './tooltip-helpers';
@@ -104,6 +104,106 @@ describe('createTooltip', () => {
expect(onOpenChange).not.toHaveBeenCalled();
});
describe('touch pointer suppression', () => {
beforeEach(() => {
vi.useFakeTimers();
// jsdom lacks matchMedia — stub so popover's canHover() returns true,
// allowing us to test that the tooltip layer blocks touch independently.
vi.stubGlobal('matchMedia', (query: string) => ({
matches: query === '(hover: hover)',
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}));
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
it('does not open on touch pointer enter', () => {
const { tooltip, onOpenChange } = createTestTooltip();
tooltip.triggerProps.onPointerEnter({
clientX: 0,
clientY: 0,
pointerId: 1,
pointerType: 'touch',
buttons: 0,
preventDefault: vi.fn(),
});
vi.advanceTimersByTime(600);
expect(onOpenChange).not.toHaveBeenCalled();
});
it('opens on mouse pointer enter', () => {
const { tooltip, onOpenChange } = createTestTooltip();
tooltip.triggerProps.onPointerEnter({
clientX: 0,
clientY: 0,
pointerId: 1,
pointerType: 'mouse',
buttons: 0,
preventDefault: vi.fn(),
});
vi.advanceTimersByTime(600);
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'hover' });
});
it('does not open via focus after pointer down (tap)', () => {
const { tooltip, onOpenChange } = createTestTooltip();
const pointerEvent = {
clientX: 0,
clientY: 0,
pointerId: 1,
pointerType: 'touch' as const,
buttons: 0,
preventDefault: vi.fn(),
};
// Simulate tap: pointerdown → focusin (flag consumed and reset)
tooltip.triggerProps.onPointerDown(pointerEvent);
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
expect(onOpenChange).not.toHaveBeenCalled();
});
it('opens via focus when no pointer down (keyboard Tab)', () => {
const { tooltip, onOpenChange } = createTestTooltip();
// Simulate keyboard Tab: focusin without preceding pointerdown
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'focus' });
});
it('opens via keyboard focus after tap-triggered focus was suppressed', () => {
const { tooltip, onOpenChange } = createTestTooltip();
const pointerEvent = {
clientX: 0,
clientY: 0,
pointerId: 1,
pointerType: 'touch' as const,
buttons: 0,
preventDefault: vi.fn(),
};
// Tap: pointerdown → focusin (suppressed, flag consumed)
tooltip.triggerProps.onPointerDown(pointerEvent);
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
expect(onOpenChange).not.toHaveBeenCalled();
// Later keyboard Tab: flag is clean, focus opens tooltip
tooltip.triggerProps.onFocusIn({ relatedTarget: null, preventDefault: vi.fn() });
expect(onOpenChange).toHaveBeenCalledWith(true, { reason: 'focus' });
});
});
it('does not open via focus when disabled', () => {
const { tooltip, onOpenChange } = createTestTooltip({
disabled: () => true,
+18 -2
View File
@@ -1,4 +1,5 @@
import type { TooltipGroupCore } from '../../../core/ui/tooltip/tooltip-group-core';
import type { UIPointerEvent } from '../event';
import {
createPopover,
type PopoverApi,
@@ -27,7 +28,9 @@ export interface TooltipOptions {
group?: () => TooltipGroupCore | undefined;
}
export interface TooltipTriggerProps extends Omit<PopoverTriggerProps, 'onClick'> {}
export interface TooltipTriggerProps extends Omit<PopoverTriggerProps, 'onClick'> {
onPointerDown: (event: UIPointerEvent) => void;
}
export interface TooltipPopupProps extends PopoverPopupProps {}
@@ -80,16 +83,29 @@ export function createTooltip(options: TooltipOptions): TooltipApi {
const popover = createPopover(popoverOpts);
// Spread popover trigger props, omit onClick, guard disabled on open handlers.
// Track whether a pointer is currently down so focus-triggered opens can be
// suppressed during tap. The browser fires pointerdown → focus → pointerup,
// so the flag is true during tap-triggered focus but false during keyboard Tab.
let isPointerDown = false;
// Spread popover trigger props, omit onClick, guard disabled/touch on open handlers.
const { onClick: _, ...baseTriggerProps } = popover.triggerProps;
const triggerProps: TooltipTriggerProps = {
...baseTriggerProps,
onPointerDown() {
isPointerDown = true;
},
onPointerEnter(event) {
if (options.disabled?.()) return;
if (event.pointerType === 'touch') return;
baseTriggerProps.onPointerEnter(event);
},
onFocusIn(event) {
if (options.disabled?.()) return;
if (isPointerDown) {
isPointerDown = false;
return;
}
baseTriggerProps.onFocusIn(event);
},
};