mirror of
https://github.com/zoriya/v10.git
synced 2026-08-14 18:04:49 +00:00
fix(core): suppress tooltip hover on touch pointer events (#933)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user