fix(react): prevent gesture tap from firing on slider interactions (#1361)

This commit is contained in:
rahim
2026-04-17 18:01:26 -07:00
committed by GitHub
parent 31a005eeef
commit 769d436f4e
4 changed files with 58 additions and 3 deletions
+50 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { DATA_ATTRS, SELECTORS } from '../fixtures/selectors';
import { DATA_ATTRS } from '../fixtures/selectors';
import { PlayerPage } from '../page-objects/player';
/**
@@ -52,6 +52,55 @@ test.describe('Mouse Gestures', () => {
await player.playButton.click();
await expect(player.playButton).not.toHaveAttribute(DATA_ATTRS.paused, { timeout: 5_000 });
});
test('click on slider does not trigger container gesture', async ({ page }) => {
// Start playback so the slider has a seekable range
await player.play();
await page.waitForTimeout(500);
// Click the time slider — should seek, not toggle play/pause.
// Wait briefly after seek to give any leaked gesture time to fire.
await player.seekTo(50);
await page.waitForTimeout(300);
await expect(player.playButton).not.toHaveAttribute(DATA_ATTRS.paused);
});
});
// --- React gestures (verify slider interaction isolation) ---
test.describe('React Mouse Gestures', () => {
let player: PlayerPage;
test.beforeEach(async ({ page }) => {
player = new PlayerPage(page);
await page.goto('/pages/react-video-mp4.html');
await player.waitForMediaReady();
});
test('click center of container toggles play/pause', async ({ page }) => {
await expect(player.playButton).toHaveAttribute(DATA_ATTRS.paused, '');
const { x, y } = await getCenter(player);
await page.mouse.click(x, y);
await expect(player.playButton).not.toHaveAttribute(DATA_ATTRS.paused, { timeout: 5_000 });
});
test('click on button does not trigger container gesture', async () => {
await expect(player.playButton).toHaveAttribute(DATA_ATTRS.paused, '');
await player.playButton.click();
await expect(player.playButton).not.toHaveAttribute(DATA_ATTRS.paused, { timeout: 5_000 });
});
test('click on slider does not trigger container gesture', async ({ page }) => {
await player.play();
await page.waitForTimeout(500);
// Click the time slider — should seek, not toggle play/pause.
// Wait briefly after seek to give any leaked gesture time to fire.
await player.seekTo(50);
await page.waitForTimeout(300);
await expect(player.playButton).not.toHaveAttribute(DATA_ATTRS.paused);
});
});
// --- Touch gestures (pointer="touch") ---
@@ -85,6 +85,7 @@ export const controlsFeature = definePlayerFeature({
if (event.pointerType === 'touch' && Date.now() - pointerDownTime < TAP_THRESHOLD) {
// When a toggleControls touch tap gesture is registered, it handles toggle — skip inline handler.
const coordinator = findGestureCoordinator(container as HTMLElement);
if (
coordinator?.bindings.some(
(b) => b.type === 'tap' && b.action === 'toggleControls' && (!b.pointer || b.pointer === 'touch')
+5 -1
View File
@@ -215,10 +215,14 @@ export function createSlider(options: SliderOptions): SliderApi {
},
onPointerUp(event) {
if (isNull(capturedPointerId)) return;
if (options.isDisabled()) return;
// The slider fully owns pointer interactions — prevent parent gesture
// coordinators from misinterpreting slider taps as surface gestures.
event.stopPropagation();
if (isNull(capturedPointerId)) return;
const percent = getPercentFromPointerEvent(event, cachedRect!, options.getOrientation(), cachedRTL);
// Cancel any pending throttled change before the final unthrottled pair.
+2 -1
View File
@@ -1,6 +1,7 @@
import { resolveEventTarget } from './event';
export const INTERACTIVE_SELECTOR = 'button,input,select,textarea,a[href],[role="slider"],[role="button"]';
export const INTERACTIVE_SELECTOR =
'button,input,select,textarea,a[href],[role="slider"],[role="button"],[data-interactive]';
const EDITABLE_INPUT_TYPES = ['text', 'search', 'url', 'tel', 'email', 'password', 'number'];