mirror of
https://github.com/zoriya/v10.git
synced 2026-08-15 18:34:22 +00:00
refactor(core): replace document listeners with pointer capture in slider (#762)
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { createState, type State } from '@videojs/store';
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
import { throttle } from '@videojs/utils/function';
|
||||
import { clamp, roundToStep } from '@videojs/utils/number';
|
||||
import { isNull } from '@videojs/utils/predicate';
|
||||
@@ -42,7 +41,14 @@ export interface SliderOptions {
|
||||
export interface SliderRootProps {
|
||||
onPointerDown: (event: UIPointerEvent) => void;
|
||||
onPointerMove: (event: UIPointerEvent) => void;
|
||||
onPointerUp: (event: UIPointerEvent) => void;
|
||||
onPointerLeave: (event: UIPointerEvent) => void;
|
||||
onLostPointerCapture: () => void;
|
||||
}
|
||||
|
||||
export interface SliderRootStyle extends Record<string, string> {
|
||||
touchAction: string;
|
||||
userSelect: string;
|
||||
}
|
||||
|
||||
export interface SliderThumbProps {
|
||||
@@ -54,6 +60,7 @@ export interface SliderThumbProps {
|
||||
export interface SliderApi {
|
||||
input: State<SliderInput>;
|
||||
rootProps: SliderRootProps;
|
||||
rootStyle: SliderRootStyle;
|
||||
thumbProps: SliderThumbProps;
|
||||
/**
|
||||
* Adjust `fillPercent` and `pointerPercent` for edge thumb alignment using
|
||||
@@ -83,7 +90,6 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
moveCount = 0,
|
||||
cachedRTL = false,
|
||||
cachedRect: DOMRect | null = null,
|
||||
documentCleanup: (() => void) | null = null,
|
||||
capturedPointerId: number | null = null;
|
||||
|
||||
const throttledCommit =
|
||||
@@ -116,60 +122,10 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
|
||||
function cleanup() {
|
||||
throttledCommit?.cancel();
|
||||
releaseCapture();
|
||||
documentCleanup?.();
|
||||
documentCleanup = null;
|
||||
capturedPointerId = null;
|
||||
cachedRect = null;
|
||||
}
|
||||
|
||||
function onDocumentPointerMove(event: PointerEvent): void {
|
||||
// Stale drag safety: if buttons === 0 for non-touch, browser lost the pointerup.
|
||||
if (event.pointerType !== 'touch' && event.buttons === 0) {
|
||||
endDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
moveCount++;
|
||||
|
||||
const percent = getPercentFromPointerEvent(event, cachedRect!, options.getOrientation(), cachedRTL);
|
||||
|
||||
if (!isDragging && moveCount >= DRAG_THRESHOLD) {
|
||||
isDragging = true;
|
||||
input.patch({ dragging: true, dragPercent: percent, pointerPercent: percent });
|
||||
options.onDragStart?.();
|
||||
options.onValueChange?.(percent);
|
||||
throttledCommit?.(percent);
|
||||
} else if (isDragging) {
|
||||
input.patch({ dragPercent: percent, pointerPercent: percent });
|
||||
options.onValueChange?.(percent);
|
||||
throttledCommit?.(percent);
|
||||
} else {
|
||||
// Below drag threshold — update hover preview only.
|
||||
input.patch({ pointerPercent: percent });
|
||||
}
|
||||
}
|
||||
|
||||
function onDocumentPointerUp(event: PointerEvent): void {
|
||||
const percent = getPercentFromPointerEvent(event, cachedRect!, options.getOrientation(), cachedRTL);
|
||||
|
||||
// Cancel pending throttled commit before the final unthrottled one.
|
||||
throttledCommit?.cancel();
|
||||
options.onValueCommit?.(percent);
|
||||
endDrag();
|
||||
}
|
||||
|
||||
function addDocumentListeners(): void {
|
||||
const abort = new AbortController();
|
||||
const signal = abort.signal;
|
||||
|
||||
listen(document, 'pointermove', onDocumentPointerMove, { passive: true, signal });
|
||||
listen(document, 'pointerup', onDocumentPointerUp, { signal });
|
||||
listen(document, 'pointercancel', endDrag, { signal });
|
||||
listen(document, 'touchmove', (e) => e.preventDefault(), { passive: false, signal });
|
||||
|
||||
documentCleanup = () => abort.abort();
|
||||
}
|
||||
|
||||
// --- Root props ---
|
||||
const rootProps: SliderRootProps = {
|
||||
onPointerDown(event) {
|
||||
@@ -192,14 +148,42 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
|
||||
// Focus the thumb for keyboard follow-up and screen reader tracking.
|
||||
options.getThumbElement?.()?.focus();
|
||||
|
||||
documentCleanup?.();
|
||||
addDocumentListeners();
|
||||
},
|
||||
|
||||
onPointerMove(event) {
|
||||
if (options.isDisabled() || isDragging) return;
|
||||
if (options.isDisabled()) return;
|
||||
|
||||
// Pointer is captured — this is a drag-related move.
|
||||
if (!isNull(capturedPointerId)) {
|
||||
// Stale drag safety: if buttons === 0 for non-touch, browser lost the pointerup.
|
||||
if (event.pointerType !== 'touch' && event.buttons === 0) {
|
||||
endDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
moveCount++;
|
||||
|
||||
const percent = getPercentFromPointerEvent(event, cachedRect!, options.getOrientation(), cachedRTL);
|
||||
|
||||
if (!isDragging && moveCount >= DRAG_THRESHOLD) {
|
||||
isDragging = true;
|
||||
input.patch({ dragging: true, dragPercent: percent, pointerPercent: percent });
|
||||
options.onDragStart?.();
|
||||
options.onValueChange?.(percent);
|
||||
throttledCommit?.(percent);
|
||||
} else if (isDragging) {
|
||||
input.patch({ dragPercent: percent, pointerPercent: percent });
|
||||
options.onValueChange?.(percent);
|
||||
throttledCommit?.(percent);
|
||||
} else {
|
||||
// Below drag threshold — update hover preview only.
|
||||
input.patch({ pointerPercent: percent });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// No capture — hover preview.
|
||||
const el = options.getElement();
|
||||
const rect = el.getBoundingClientRect();
|
||||
const percent = getPercentFromPointerEvent(event, rect, options.getOrientation(), options.isRTL());
|
||||
@@ -207,10 +191,24 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
input.patch({ pointing: true, pointerPercent: percent });
|
||||
},
|
||||
|
||||
onPointerUp(event) {
|
||||
if (isNull(capturedPointerId)) return;
|
||||
|
||||
const percent = getPercentFromPointerEvent(event, cachedRect!, options.getOrientation(), cachedRTL);
|
||||
|
||||
// Cancel pending throttled commit before the final unthrottled one.
|
||||
throttledCommit?.cancel();
|
||||
options.onValueCommit?.(percent);
|
||||
},
|
||||
|
||||
onPointerLeave() {
|
||||
if (isDragging) return;
|
||||
if (!isNull(capturedPointerId)) return;
|
||||
input.patch({ pointing: false, pointerPercent: 0 });
|
||||
},
|
||||
|
||||
onLostPointerCapture() {
|
||||
endDrag();
|
||||
},
|
||||
};
|
||||
|
||||
// --- Thumb props ---
|
||||
@@ -306,15 +304,19 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
};
|
||||
}
|
||||
|
||||
listen(abort.signal, 'abort', cleanup, { once: true });
|
||||
const rootStyle: SliderRootStyle = { touchAction: 'none', userSelect: 'none' };
|
||||
|
||||
return {
|
||||
input,
|
||||
rootProps,
|
||||
rootStyle,
|
||||
thumbProps,
|
||||
adjustForAlignment,
|
||||
destroy() {
|
||||
if (abort.signal.aborted) return;
|
||||
abort.abort();
|
||||
releaseCapture();
|
||||
cleanup();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { flush } from '@videojs/store';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { UIKeyboardEvent, UIPointerEvent } from '../event';
|
||||
import { createSlider, type SliderOptions } from '../slider';
|
||||
import { createSlider, type SliderApi, type SliderOptions } from '../slider';
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
@@ -73,18 +73,19 @@ function keyboardEvent(key: string, overrides: Partial<UIKeyboardEvent> = {}): U
|
||||
};
|
||||
}
|
||||
|
||||
function fireDocumentPointerMove(overrides: Partial<UIPointerEvent> = {}): void {
|
||||
const init = pointerEvent(overrides);
|
||||
document.dispatchEvent(new PointerEvent('pointermove', init));
|
||||
/** Simulate a pointermove on the element (routed via pointer capture during drag). */
|
||||
function firePointerMove(slider: SliderApi, overrides: Partial<UIPointerEvent> = {}): void {
|
||||
slider.rootProps.onPointerMove(pointerEvent(overrides));
|
||||
}
|
||||
|
||||
function fireDocumentPointerUp(overrides: Partial<UIPointerEvent> = {}): void {
|
||||
const init = pointerEvent({ buttons: 0, ...overrides });
|
||||
document.dispatchEvent(new PointerEvent('pointerup', init));
|
||||
/** Simulate a pointerup on the element (routed via pointer capture). */
|
||||
function firePointerUp(slider: SliderApi, overrides: Partial<UIPointerEvent> = {}): void {
|
||||
slider.rootProps.onPointerUp(pointerEvent({ buttons: 0, ...overrides }));
|
||||
}
|
||||
|
||||
function fireDocumentPointerCancel(): void {
|
||||
document.dispatchEvent(new PointerEvent('pointercancel'));
|
||||
/** Simulate lostpointercapture — fires after pointerup or pointercancel. */
|
||||
function fireLostPointerCapture(slider: SliderApi): void {
|
||||
slider.rootProps.onLostPointerCapture();
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
@@ -200,13 +201,13 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
|
||||
// First move — below threshold
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
flush();
|
||||
expect(slider.input.current.dragging).toBe(false);
|
||||
expect(onDragStart).not.toHaveBeenCalled();
|
||||
|
||||
// Second move — meets threshold
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
flush();
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
expect(onDragStart).toHaveBeenCalledOnce();
|
||||
@@ -223,15 +224,15 @@ describe('createSlider', () => {
|
||||
onValueChange.mockClear();
|
||||
|
||||
// Move 1: below threshold — no onValueChange
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
expect(onValueChange).not.toHaveBeenCalled();
|
||||
|
||||
// Move 2: meets threshold — onValueChange fires
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
expect(onValueChange).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Move 3: during drag — onValueChange fires
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
firePointerMove(slider, { clientX: 100 });
|
||||
expect(onValueChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
slider.destroy();
|
||||
@@ -242,8 +243,8 @@ describe('createSlider', () => {
|
||||
const slider = createSlider(createOptions({ getElement: () => el }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 100 });
|
||||
flush();
|
||||
|
||||
expect(slider.input.current.dragPercent).toBe(50);
|
||||
@@ -253,19 +254,24 @@ describe('createSlider', () => {
|
||||
});
|
||||
|
||||
describe('pointer: pointerup', () => {
|
||||
it('calls onValueCommit and onDragEnd on pointerup', () => {
|
||||
it('calls onValueCommit on pointerup and onDragEnd on lostpointercapture', () => {
|
||||
const onValueCommit = vi.fn();
|
||||
const onDragEnd = vi.fn();
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el, onValueCommit, onDragEnd }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
fireDocumentPointerUp({ clientX: 100 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
|
||||
// pointerup commits the value.
|
||||
firePointerUp(slider, { clientX: 100 });
|
||||
expect(onValueCommit).toHaveBeenCalledWith(50);
|
||||
|
||||
// lostpointercapture cleans up drag state.
|
||||
fireLostPointerCapture(slider);
|
||||
flush();
|
||||
|
||||
expect(onValueCommit).toHaveBeenCalledWith(50);
|
||||
expect(onDragEnd).toHaveBeenCalled();
|
||||
expect(slider.input.current.dragging).toBe(false);
|
||||
expect(slider.input.current.pointing).toBe(false);
|
||||
@@ -273,25 +279,13 @@ describe('createSlider', () => {
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
it('releases pointer capture on pointerup', () => {
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ pointerId: 42, clientX: 50 }));
|
||||
fireDocumentPointerUp({ clientX: 100 });
|
||||
|
||||
expect(el.releasePointerCapture).toHaveBeenCalledWith(42);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
it('calls onValueCommit on pointerup even without drag', () => {
|
||||
const onValueCommit = vi.fn();
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el, onValueCommit }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 100 }));
|
||||
fireDocumentPointerUp({ clientX: 100 });
|
||||
firePointerUp(slider, { clientX: 100 });
|
||||
|
||||
expect(onValueCommit).toHaveBeenCalledWith(50);
|
||||
|
||||
@@ -299,16 +293,18 @@ describe('createSlider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('pointer: pointercancel', () => {
|
||||
it('ends drag on pointercancel', () => {
|
||||
describe('pointer: lostpointercapture', () => {
|
||||
it('ends drag on lostpointercapture (e.g., after pointercancel)', () => {
|
||||
const onDragEnd = vi.fn();
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el, onDragEnd }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
fireDocumentPointerCancel();
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
|
||||
// Browser fires lostpointercapture after pointercancel or other capture loss.
|
||||
fireLostPointerCapture(slider);
|
||||
flush();
|
||||
|
||||
expect(onDragEnd).toHaveBeenCalled();
|
||||
@@ -317,14 +313,20 @@ describe('createSlider', () => {
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
it('releases pointer capture on pointercancel', () => {
|
||||
it('resets pointing state when no drag occurred', () => {
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ pointerId: 42, clientX: 50 }));
|
||||
fireDocumentPointerCancel();
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
flush();
|
||||
expect(slider.input.current.pointing).toBe(true);
|
||||
|
||||
expect(el.releasePointerCapture).toHaveBeenCalledWith(42);
|
||||
// Lost capture without crossing drag threshold.
|
||||
fireLostPointerCapture(slider);
|
||||
flush();
|
||||
|
||||
expect(slider.input.current.pointing).toBe(false);
|
||||
expect(slider.input.current.pointerPercent).toBe(0);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
@@ -337,13 +339,13 @@ describe('createSlider', () => {
|
||||
const slider = createSlider(createOptions({ getElement: () => el, onDragEnd }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
flush();
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
|
||||
// Stale: buttons = 0, mouse pointer
|
||||
fireDocumentPointerMove({ clientX: 100, buttons: 0, pointerType: 'mouse' });
|
||||
firePointerMove(slider, { clientX: 100, buttons: 0, pointerType: 'mouse' });
|
||||
flush();
|
||||
|
||||
expect(slider.input.current.dragging).toBe(false);
|
||||
@@ -352,18 +354,6 @@ describe('createSlider', () => {
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
it('releases pointer capture on stale drag', () => {
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ pointerId: 42, clientX: 50 }));
|
||||
fireDocumentPointerMove({ clientX: 60, buttons: 0, pointerType: 'mouse' });
|
||||
|
||||
expect(el.releasePointerCapture).toHaveBeenCalledWith(42);
|
||||
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
it('does not end drag for touch pointer with buttons 0', () => {
|
||||
const onDragEnd = vi.fn();
|
||||
const onValueChange = vi.fn();
|
||||
@@ -371,13 +361,13 @@ describe('createSlider', () => {
|
||||
const slider = createSlider(createOptions({ getElement: () => el, onDragEnd, onValueChange }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
flush();
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
|
||||
// Touch with buttons=0 should NOT trigger stale drag detection
|
||||
fireDocumentPointerMove({ clientX: 100, buttons: 0, pointerType: 'touch' });
|
||||
firePointerMove(slider, { clientX: 100, buttons: 0, pointerType: 'touch' });
|
||||
flush();
|
||||
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
@@ -415,16 +405,17 @@ describe('createSlider', () => {
|
||||
slider.destroy();
|
||||
});
|
||||
|
||||
it('does not reset on pointerleave during drag', () => {
|
||||
it('does not reset on pointerleave while pointer is captured', () => {
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
flush();
|
||||
expect(slider.input.current.dragging).toBe(true);
|
||||
|
||||
// pointerleave is suppressed during capture; if it fires, it should be ignored.
|
||||
slider.rootProps.onPointerLeave(pointerEvent());
|
||||
flush();
|
||||
|
||||
@@ -858,20 +849,6 @@ describe('createSlider', () => {
|
||||
|
||||
expect(el.releasePointerCapture).toHaveBeenCalledWith(42);
|
||||
});
|
||||
|
||||
it('destroy cleans up active document listeners', () => {
|
||||
const onDragEnd = vi.fn();
|
||||
const el = createMockElement({ left: 0, width: 200 });
|
||||
const slider = createSlider(createOptions({ getElement: () => el, onDragEnd }));
|
||||
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
slider.destroy();
|
||||
|
||||
// Document pointermove after destroy should not trigger callbacks
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
|
||||
expect(onDragEnd).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitThrottle', () => {
|
||||
@@ -884,9 +861,9 @@ describe('createSlider', () => {
|
||||
onValueCommit.mockClear();
|
||||
|
||||
// Pass drag threshold
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 100 });
|
||||
|
||||
expect(onValueCommit).not.toHaveBeenCalled();
|
||||
|
||||
@@ -904,10 +881,10 @@ describe('createSlider', () => {
|
||||
onValueCommit.mockClear();
|
||||
|
||||
// Pass drag threshold and continue dragging
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
fireDocumentPointerMove({ clientX: 120 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 100 });
|
||||
firePointerMove(slider, { clientX: 120 });
|
||||
|
||||
// Not yet — throttle hasn't fired
|
||||
expect(onValueCommit).not.toHaveBeenCalled();
|
||||
@@ -934,13 +911,13 @@ describe('createSlider', () => {
|
||||
onValueCommit.mockClear();
|
||||
|
||||
// Pass threshold
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
|
||||
// Multiple rapid moves during drag
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
fireDocumentPointerMove({ clientX: 120 });
|
||||
fireDocumentPointerMove({ clientX: 140 });
|
||||
firePointerMove(slider, { clientX: 100 });
|
||||
firePointerMove(slider, { clientX: 120 });
|
||||
firePointerMove(slider, { clientX: 140 });
|
||||
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
@@ -963,13 +940,13 @@ describe('createSlider', () => {
|
||||
onValueCommit.mockClear();
|
||||
|
||||
// Pass threshold and drag
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
vi.advanceTimersByTime(100);
|
||||
expect(onValueCommit).toHaveBeenCalledOnce();
|
||||
|
||||
// Continue dragging — should schedule another throttle
|
||||
fireDocumentPointerMove({ clientX: 140 });
|
||||
firePointerMove(slider, { clientX: 140 });
|
||||
vi.advanceTimersByTime(100);
|
||||
|
||||
expect(onValueCommit).toHaveBeenCalledTimes(2);
|
||||
@@ -990,12 +967,12 @@ describe('createSlider', () => {
|
||||
onValueCommit.mockClear();
|
||||
|
||||
// Pass threshold and drag
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
fireDocumentPointerMove({ clientX: 120 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 120 });
|
||||
|
||||
// Release before throttle fires
|
||||
fireDocumentPointerUp({ clientX: 150 });
|
||||
firePointerUp(slider, { clientX: 150 });
|
||||
|
||||
// Final commit with release position (150/200 = 75%)
|
||||
expect(onValueCommit).toHaveBeenCalledOnce();
|
||||
@@ -1020,8 +997,8 @@ describe('createSlider', () => {
|
||||
onValueCommit.mockClear();
|
||||
|
||||
// Pass threshold
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
|
||||
slider.destroy();
|
||||
|
||||
@@ -1032,7 +1009,7 @@ describe('createSlider', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('cancels throttle on pointercancel', () => {
|
||||
it('cancels throttle on lostpointercapture', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const onValueCommit = vi.fn();
|
||||
@@ -1043,10 +1020,10 @@ describe('createSlider', () => {
|
||||
onValueCommit.mockClear();
|
||||
|
||||
// Pass threshold
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
|
||||
fireDocumentPointerCancel();
|
||||
fireLostPointerCapture(slider);
|
||||
|
||||
// Advancing timer should NOT fire
|
||||
vi.advanceTimersByTime(200);
|
||||
@@ -1080,9 +1057,9 @@ describe('createSlider', () => {
|
||||
slider.rootProps.onPointerDown(pointerEvent({ clientX: 50 }));
|
||||
onValueCommit.mockClear();
|
||||
|
||||
fireDocumentPointerMove({ clientX: 60 });
|
||||
fireDocumentPointerMove({ clientX: 80 });
|
||||
fireDocumentPointerMove({ clientX: 100 });
|
||||
firePointerMove(slider, { clientX: 60 });
|
||||
firePointerMove(slider, { clientX: 80 });
|
||||
firePointerMove(slider, { clientX: 100 });
|
||||
|
||||
vi.advanceTimersByTime(200);
|
||||
|
||||
|
||||
@@ -78,11 +78,8 @@ export class SliderElement extends MediaElement {
|
||||
});
|
||||
|
||||
applyElementProps(this, this.#slider.rootProps, { signal });
|
||||
applyStyles(this, this.#slider.rootStyle);
|
||||
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
|
||||
|
||||
// Prevent default touch gestures and text selection during interaction.
|
||||
this.style.touchAction = 'none';
|
||||
this.style.userSelect = 'none';
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
|
||||
@@ -83,12 +83,9 @@ export class TimeSliderElement extends MediaElement {
|
||||
});
|
||||
|
||||
applyElementProps(this, this.#slider.rootProps, { signal });
|
||||
applyStyles(this, this.#slider.rootStyle);
|
||||
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
|
||||
|
||||
// Prevent default touch gestures and text selection during interaction.
|
||||
this.style.touchAction = 'none';
|
||||
this.style.userSelect = 'none';
|
||||
|
||||
if (__DEV__ && !this.#timeState.value) {
|
||||
logMissingFeature(this.localName, this.#timeState.displayName!);
|
||||
}
|
||||
|
||||
@@ -79,12 +79,9 @@ export class VolumeSliderElement extends MediaElement {
|
||||
});
|
||||
|
||||
applyElementProps(this, this.#slider.rootProps, { signal });
|
||||
applyStyles(this, this.#slider.rootStyle);
|
||||
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
|
||||
|
||||
// Prevent default touch gestures and text selection during interaction.
|
||||
this.style.touchAction = 'none';
|
||||
this.style.userSelect = 'none';
|
||||
|
||||
if (__DEV__ && !this.#volumeState.value) {
|
||||
logMissingFeature(this.localName, this.#volumeState.displayName!);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type SliderApi,
|
||||
type SliderOptions,
|
||||
type SliderRootProps,
|
||||
type SliderRootStyle,
|
||||
type SliderThumbProps,
|
||||
} from '@videojs/core/dom';
|
||||
import { useSnapshot } from '@videojs/store/react';
|
||||
@@ -42,6 +43,7 @@ export interface UseSliderReturnValue<State extends SliderState = SliderState> {
|
||||
rootRef: React.RefCallback<HTMLElement>;
|
||||
thumbRef: React.RefCallback<HTMLElement>;
|
||||
rootProps: SliderRootProps;
|
||||
rootStyle: SliderRootStyle;
|
||||
thumbProps: SliderThumbProps;
|
||||
}
|
||||
|
||||
@@ -117,6 +119,7 @@ export function useSlider<State extends SliderState = SliderState>(
|
||||
rootRef,
|
||||
thumbRef,
|
||||
rootProps: slider.rootProps,
|
||||
rootStyle: slider.rootStyle,
|
||||
thumbProps: slider.thumbProps,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export const SliderRoot = forwardRef(function SliderRoot(
|
||||
rootRef,
|
||||
thumbRef: sliderThumbRef,
|
||||
rootProps,
|
||||
rootStyle,
|
||||
thumbProps,
|
||||
} = useSlider({
|
||||
computeState: (input) => {
|
||||
@@ -89,7 +90,7 @@ export const SliderRoot = forwardRef(function SliderRoot(
|
||||
state,
|
||||
stateAttrMap: SliderDataAttrs,
|
||||
ref: [forwardedRef, rootRef],
|
||||
props: [{ style: cssVars }, rootProps, elementProps],
|
||||
props: [{ style: { ...cssVars, ...rootStyle } }, rootProps, elementProps],
|
||||
}
|
||||
)}
|
||||
</SliderProvider>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const TimeSliderRoot = forwardRef<HTMLDivElement, TimeSliderRootProps>(
|
||||
|
||||
const duration = time?.duration ?? 0;
|
||||
|
||||
const { state, cssVars, rootRef, thumbRef, rootProps, thumbProps } = useSlider<TimeSliderCore.State>({
|
||||
const { state, cssVars, rootRef, thumbRef, rootProps, rootStyle, thumbProps } = useSlider<TimeSliderCore.State>({
|
||||
computeState: (input) => {
|
||||
core.setInput(input);
|
||||
if (!time || !buffer) {
|
||||
@@ -107,7 +107,7 @@ export const TimeSliderRoot = forwardRef<HTMLDivElement, TimeSliderRootProps>(
|
||||
state,
|
||||
stateAttrMap: TimeSliderDataAttrs,
|
||||
ref: [forwardedRef, rootRef],
|
||||
props: [{ style: cssVars }, rootProps, elementProps],
|
||||
props: [{ style: { ...cssVars, ...rootStyle } }, rootProps, elementProps],
|
||||
}
|
||||
)}
|
||||
</SliderProvider>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const VolumeSliderRoot = forwardRef<HTMLDivElement, VolumeSliderRootProps
|
||||
// Keep a ref to the latest volume state for callbacks.
|
||||
const volumeRef = useLatestRef(volume);
|
||||
|
||||
const { state, cssVars, rootRef, thumbRef, rootProps, thumbProps } = useSlider<VolumeSliderCore.State>({
|
||||
const { state, cssVars, rootRef, thumbRef, rootProps, rootStyle, thumbProps } = useSlider<VolumeSliderCore.State>({
|
||||
computeState: (input) => {
|
||||
core.setInput(input);
|
||||
core.setMedia(volume ?? noopVolume);
|
||||
@@ -97,7 +97,7 @@ export const VolumeSliderRoot = forwardRef<HTMLDivElement, VolumeSliderRootProps
|
||||
state,
|
||||
stateAttrMap: SliderDataAttrs,
|
||||
ref: [forwardedRef, rootRef],
|
||||
props: [{ style: cssVars }, rootProps, elementProps],
|
||||
props: [{ style: { ...cssVars, ...rootStyle } }, rootProps, elementProps],
|
||||
}
|
||||
)}
|
||||
</SliderProvider>
|
||||
|
||||
Reference in New Issue
Block a user