diff --git a/packages/core/src/dom/ui/slider.ts b/packages/core/src/dom/ui/slider.ts index 486f9e95..29c97aa5 100644 --- a/packages/core/src/dom/ui/slider.ts +++ b/packages/core/src/dom/ui/slider.ts @@ -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 { + touchAction: string; + userSelect: string; } export interface SliderThumbProps { @@ -54,6 +60,7 @@ export interface SliderThumbProps { export interface SliderApi { input: State; 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(); }, }; } diff --git a/packages/core/src/dom/ui/tests/slider.test.ts b/packages/core/src/dom/ui/tests/slider.test.ts index 11f78a1e..7edfcbac 100644 --- a/packages/core/src/dom/ui/tests/slider.test.ts +++ b/packages/core/src/dom/ui/tests/slider.test.ts @@ -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 = {}): U }; } -function fireDocumentPointerMove(overrides: Partial = {}): 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 = {}): void { + slider.rootProps.onPointerMove(pointerEvent(overrides)); } -function fireDocumentPointerUp(overrides: Partial = {}): 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 = {}): 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); diff --git a/packages/html/src/ui/slider/slider-element.ts b/packages/html/src/ui/slider/slider-element.ts index 99aad9be..c4c93925 100644 --- a/packages/html/src/ui/slider/slider-element.ts +++ b/packages/html/src/ui/slider/slider-element.ts @@ -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 { diff --git a/packages/html/src/ui/time-slider/time-slider-element.ts b/packages/html/src/ui/time-slider/time-slider-element.ts index 8f748fbe..cef467f7 100644 --- a/packages/html/src/ui/time-slider/time-slider-element.ts +++ b/packages/html/src/ui/time-slider/time-slider-element.ts @@ -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!); } diff --git a/packages/html/src/ui/volume-slider/volume-slider-element.ts b/packages/html/src/ui/volume-slider/volume-slider-element.ts index 548d831e..ea6a2954 100644 --- a/packages/html/src/ui/volume-slider/volume-slider-element.ts +++ b/packages/html/src/ui/volume-slider/volume-slider-element.ts @@ -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!); } diff --git a/packages/react/src/ui/hooks/use-slider.ts b/packages/react/src/ui/hooks/use-slider.ts index 12d102e9..e5309e9d 100644 --- a/packages/react/src/ui/hooks/use-slider.ts +++ b/packages/react/src/ui/hooks/use-slider.ts @@ -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 { rootRef: React.RefCallback; thumbRef: React.RefCallback; rootProps: SliderRootProps; + rootStyle: SliderRootStyle; thumbProps: SliderThumbProps; } @@ -117,6 +119,7 @@ export function useSlider( rootRef, thumbRef, rootProps: slider.rootProps, + rootStyle: slider.rootStyle, thumbProps: slider.thumbProps, }; } diff --git a/packages/react/src/ui/slider/slider-root.tsx b/packages/react/src/ui/slider/slider-root.tsx index 821b9636..60f7d1ce 100644 --- a/packages/react/src/ui/slider/slider-root.tsx +++ b/packages/react/src/ui/slider/slider-root.tsx @@ -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], } )} diff --git a/packages/react/src/ui/time-slider/time-slider-root.tsx b/packages/react/src/ui/time-slider/time-slider-root.tsx index b5c17636..16614ebe 100644 --- a/packages/react/src/ui/time-slider/time-slider-root.tsx +++ b/packages/react/src/ui/time-slider/time-slider-root.tsx @@ -48,7 +48,7 @@ export const TimeSliderRoot = forwardRef( const duration = time?.duration ?? 0; - const { state, cssVars, rootRef, thumbRef, rootProps, thumbProps } = useSlider({ + const { state, cssVars, rootRef, thumbRef, rootProps, rootStyle, thumbProps } = useSlider({ computeState: (input) => { core.setInput(input); if (!time || !buffer) { @@ -107,7 +107,7 @@ export const TimeSliderRoot = forwardRef( state, stateAttrMap: TimeSliderDataAttrs, ref: [forwardedRef, rootRef], - props: [{ style: cssVars }, rootProps, elementProps], + props: [{ style: { ...cssVars, ...rootStyle } }, rootProps, elementProps], } )} diff --git a/packages/react/src/ui/volume-slider/volume-slider-root.tsx b/packages/react/src/ui/volume-slider/volume-slider-root.tsx index 02e529fb..19a96324 100644 --- a/packages/react/src/ui/volume-slider/volume-slider-root.tsx +++ b/packages/react/src/ui/volume-slider/volume-slider-root.tsx @@ -49,7 +49,7 @@ export const VolumeSliderRoot = forwardRef({ + const { state, cssVars, rootRef, thumbRef, rootProps, rootStyle, thumbProps } = useSlider({ computeState: (input) => { core.setInput(input); core.setMedia(volume ?? noopVolume); @@ -97,7 +97,7 @@ export const VolumeSliderRoot = forwardRef