refactor(core): replace document listeners with pointer capture in slider (#762)

This commit is contained in:
rahim
2026-03-06 13:42:31 -08:00
committed by GitHub
parent 814ba017e4
commit f2f9e6ddc3
9 changed files with 156 additions and 182 deletions
+61 -59
View File
@@ -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();
},
};
}
+83 -106
View File
@@ -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,
};
}
+2 -1
View File
@@ -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>