mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): volume slider scroll support (#1175)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ describe('VolumeSliderCore', () => {
|
||||
label: 'Volume',
|
||||
step: 1,
|
||||
largeStep: 10,
|
||||
wheelStep: 5,
|
||||
orientation: 'horizontal',
|
||||
disabled: false,
|
||||
thumbAlignment: 'center',
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { MediaFeatureAvailability, MediaVolumeState } from '../../media/sta
|
||||
import { SliderCore, type SliderProps, type SliderState } from '../slider/slider-core';
|
||||
|
||||
export interface VolumeSliderProps extends SliderProps {
|
||||
/** Step increment for wheel scrolling. */
|
||||
wheelStep?: number | undefined;
|
||||
/** @internal Derived from `volume` (0–100) — not user-settable. */
|
||||
value?: number | undefined;
|
||||
/** @internal Always 0 — not user-settable. */
|
||||
@@ -22,6 +24,7 @@ export class VolumeSliderCore extends SliderCore {
|
||||
static override readonly defaultProps: NonNullableObject<VolumeSliderProps> = {
|
||||
...SliderCore.defaultProps,
|
||||
label: 'Volume',
|
||||
wheelStep: 5,
|
||||
};
|
||||
|
||||
#media: MediaVolumeState | null = null;
|
||||
@@ -57,6 +60,13 @@ export class VolumeSliderCore extends SliderCore {
|
||||
};
|
||||
}
|
||||
|
||||
/** Wheel step as a percentage of the slider range. */
|
||||
getWheelStepPercent(): number {
|
||||
const props = this.props as NonNullableObject<VolumeSliderProps>;
|
||||
const range = props.max - props.min;
|
||||
return range > 0 ? (props.wheelStep / range) * 100 : 0;
|
||||
}
|
||||
|
||||
override getLabel(state: SliderState): string {
|
||||
return super.getLabel(state) || 'Volume';
|
||||
}
|
||||
|
||||
@@ -14,4 +14,5 @@ export * from './ui/slider-css-vars';
|
||||
export * from './ui/thumbnail';
|
||||
export * from './ui/tooltip/tooltip';
|
||||
export * from './ui/transition';
|
||||
export * from './ui/wheel-step';
|
||||
export * from './utils';
|
||||
|
||||
@@ -20,6 +20,10 @@ export interface UIPointerEvent extends UIEvent {
|
||||
buttons: number;
|
||||
}
|
||||
|
||||
export interface UIWheelEvent extends UIEvent {
|
||||
deltaY: number;
|
||||
}
|
||||
|
||||
export interface UIFocusEvent extends UIEvent {
|
||||
relatedTarget: EventTarget | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { UIWheelEvent } from '../event';
|
||||
import { createWheelStep, type WheelStepOptions } from '../wheel-step';
|
||||
|
||||
function wheelEvent(deltaY: number): UIWheelEvent {
|
||||
return {
|
||||
deltaY,
|
||||
preventDefault: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function createOptions(overrides: Partial<WheelStepOptions> = {}): WheelStepOptions {
|
||||
return {
|
||||
isDisabled: () => false,
|
||||
getPercent: () => 50,
|
||||
getStepPercent: () => 1,
|
||||
onValueChange: vi.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('createWheelStep', () => {
|
||||
it('scroll up increments by step', () => {
|
||||
const onValueChange = vi.fn();
|
||||
const { onWheel } = createWheelStep(
|
||||
createOptions({ getPercent: () => 50, getStepPercent: () => 1, onValueChange })
|
||||
);
|
||||
|
||||
onWheel(wheelEvent(-1));
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith(51);
|
||||
});
|
||||
|
||||
it('scroll down decrements by step', () => {
|
||||
const onValueChange = vi.fn();
|
||||
const { onWheel } = createWheelStep(
|
||||
createOptions({ getPercent: () => 50, getStepPercent: () => 1, onValueChange })
|
||||
);
|
||||
|
||||
onWheel(wheelEvent(1));
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith(49);
|
||||
});
|
||||
|
||||
it('uses step percent from options', () => {
|
||||
const onValueChange = vi.fn();
|
||||
const { onWheel } = createWheelStep(
|
||||
createOptions({ getPercent: () => 50, getStepPercent: () => 5, onValueChange })
|
||||
);
|
||||
|
||||
onWheel(wheelEvent(-1));
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith(55);
|
||||
});
|
||||
|
||||
it('clamps to 0 on scroll down at minimum', () => {
|
||||
const onValueChange = vi.fn();
|
||||
const { onWheel } = createWheelStep(createOptions({ getPercent: () => 0, getStepPercent: () => 5, onValueChange }));
|
||||
|
||||
onWheel(wheelEvent(1));
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it('clamps to 100 on scroll up at maximum', () => {
|
||||
const onValueChange = vi.fn();
|
||||
const { onWheel } = createWheelStep(
|
||||
createOptions({ getPercent: () => 100, getStepPercent: () => 5, onValueChange })
|
||||
);
|
||||
|
||||
onWheel(wheelEvent(-1));
|
||||
|
||||
expect(onValueChange).toHaveBeenCalledWith(100);
|
||||
});
|
||||
|
||||
it('no-ops when disabled', () => {
|
||||
const onValueChange = vi.fn();
|
||||
const { onWheel } = createWheelStep(createOptions({ isDisabled: () => true, onValueChange }));
|
||||
|
||||
const event = wheelEvent(-1);
|
||||
onWheel(event);
|
||||
|
||||
expect(onValueChange).not.toHaveBeenCalled();
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls preventDefault on scroll', () => {
|
||||
const { onWheel } = createWheelStep(createOptions());
|
||||
|
||||
const event = wheelEvent(-1);
|
||||
onWheel(event);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores zero deltaY', () => {
|
||||
const onValueChange = vi.fn();
|
||||
const { onWheel } = createWheelStep(createOptions({ onValueChange }));
|
||||
|
||||
const event = wheelEvent(0);
|
||||
onWheel(event);
|
||||
|
||||
expect(onValueChange).not.toHaveBeenCalled();
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { clamp } from '@videojs/utils/number';
|
||||
import type { UIWheelEvent } from './event';
|
||||
|
||||
export interface WheelStepOptions {
|
||||
isDisabled: () => boolean;
|
||||
getPercent: () => number;
|
||||
getStepPercent: () => number;
|
||||
onValueChange?: ((percent: number) => void) | undefined;
|
||||
}
|
||||
|
||||
export interface WheelStepProps {
|
||||
onWheel: (event: UIWheelEvent) => void;
|
||||
}
|
||||
|
||||
export function createWheelStep(options: WheelStepOptions): WheelStepProps {
|
||||
return {
|
||||
onWheel(event) {
|
||||
if (options.isDisabled()) return;
|
||||
|
||||
const direction = Math.sign(event.deltaY);
|
||||
if (direction === 0) return;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const stepPercent = options.getStepPercent();
|
||||
const currentPercent = options.getPercent();
|
||||
const newPercent = clamp(currentPercent - direction * stepPercent, 0, 100);
|
||||
|
||||
options.onValueChange?.(newPercent);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
applyElementProps,
|
||||
applyStateDataAttrs,
|
||||
createSlider,
|
||||
createWheelStep,
|
||||
getSliderCSSVars,
|
||||
logMissingFeature,
|
||||
type SliderApi,
|
||||
@@ -24,6 +25,7 @@ export class VolumeSliderElement extends MediaElement {
|
||||
label: { type: String },
|
||||
step: { type: Number },
|
||||
largeStep: { type: Number, attribute: 'large-step' },
|
||||
wheelStep: { type: Number, attribute: 'wheel-step' },
|
||||
orientation: { type: String },
|
||||
disabled: { type: Boolean },
|
||||
thumbAlignment: { type: String, attribute: 'thumb-alignment' },
|
||||
@@ -32,6 +34,7 @@ export class VolumeSliderElement extends MediaElement {
|
||||
label = VolumeSliderCore.defaultProps.label;
|
||||
step = VolumeSliderCore.defaultProps.step;
|
||||
largeStep = VolumeSliderCore.defaultProps.largeStep;
|
||||
wheelStep = VolumeSliderCore.defaultProps.wheelStep;
|
||||
orientation = VolumeSliderCore.defaultProps.orientation;
|
||||
disabled = VolumeSliderCore.defaultProps.disabled;
|
||||
thumbAlignment = VolumeSliderCore.defaultProps.thumbAlignment;
|
||||
@@ -50,25 +53,22 @@ export class VolumeSliderElement extends MediaElement {
|
||||
this.#disconnect = new AbortController();
|
||||
const signal = this.#disconnect.signal;
|
||||
|
||||
const isDisabled = () => this.disabled || !this.#volumeState.value;
|
||||
const getPercent = () => (this.#volumeState.value?.volume ?? 0) * 100;
|
||||
const getStepPercent = () => this.#core.getStepPercent();
|
||||
const setVolume = (percent: number) => this.#setVolume(percent);
|
||||
|
||||
this.#slider = createSlider({
|
||||
getElement: () => this,
|
||||
getThumbElement: () => this.querySelector<HTMLElement>('media-slider-thumb'),
|
||||
getOrientation: () => this.orientation,
|
||||
isRTL: () => isRTL(this),
|
||||
isDisabled: () => this.disabled || !this.#volumeState.value,
|
||||
getPercent: () => {
|
||||
const media = this.#volumeState.value;
|
||||
if (!media) return 0;
|
||||
return media.volume * 100;
|
||||
},
|
||||
getStepPercent: () => this.#core.getStepPercent(),
|
||||
isDisabled,
|
||||
getPercent,
|
||||
getStepPercent,
|
||||
getLargeStepPercent: () => this.#core.getLargeStepPercent(),
|
||||
onValueChange: (percent) => {
|
||||
this.#setVolume(percent);
|
||||
},
|
||||
onValueCommit: (percent) => {
|
||||
this.#setVolume(percent);
|
||||
},
|
||||
onValueChange: setVolume,
|
||||
onValueCommit: setVolume,
|
||||
onDragStart: () => {
|
||||
this.dispatchEvent(new CustomEvent('drag-start', { bubbles: true }));
|
||||
},
|
||||
@@ -79,7 +79,15 @@ export class VolumeSliderElement extends MediaElement {
|
||||
onResize: () => this.requestUpdate(),
|
||||
});
|
||||
|
||||
const wheelProps = createWheelStep({
|
||||
isDisabled,
|
||||
getPercent,
|
||||
getStepPercent: () => this.#core.getWheelStepPercent(),
|
||||
onValueChange: setVolume,
|
||||
});
|
||||
|
||||
applyElementProps(this, this.#slider.rootProps, { signal });
|
||||
applyElementProps(this, wheelProps, { signal });
|
||||
applyStyles(this, this.#slider.rootStyle);
|
||||
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
|
||||
|
||||
|
||||
@@ -11,39 +11,45 @@ import { VolumeSliderRoot } from '../volume-slider-root';
|
||||
|
||||
// --- Hoisted mock data (available inside vi.mock factories) ---
|
||||
|
||||
const { mockSliderApi, mockVolumeState } = vi.hoisted(() => ({
|
||||
mockSliderApi: () => ({
|
||||
input: {
|
||||
current: {
|
||||
pointerPercent: 0,
|
||||
dragPercent: 0,
|
||||
dragging: false,
|
||||
pointing: false,
|
||||
focused: false,
|
||||
},
|
||||
subscribe: vi.fn(() => vi.fn()),
|
||||
},
|
||||
rootProps: {
|
||||
onPointerDown: vi.fn(),
|
||||
onPointerMove: vi.fn(),
|
||||
onPointerLeave: vi.fn(),
|
||||
},
|
||||
thumbProps: {
|
||||
onKeyDown: vi.fn(),
|
||||
onFocus: vi.fn(),
|
||||
onBlur: vi.fn(),
|
||||
},
|
||||
adjustForAlignment: <S,>(state: S): S => state,
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
mockVolumeState: {
|
||||
const { mockSliderApi, mockVolumeState, mutableVolume } = vi.hoisted(() => {
|
||||
const volumeState = {
|
||||
volume: 0.8,
|
||||
muted: false,
|
||||
volumeAvailability: 'available' as const,
|
||||
setVolume: vi.fn(),
|
||||
toggleMuted: vi.fn(),
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
mockSliderApi: () => ({
|
||||
input: {
|
||||
current: {
|
||||
pointerPercent: 0,
|
||||
dragPercent: 0,
|
||||
dragging: false,
|
||||
pointing: false,
|
||||
focused: false,
|
||||
},
|
||||
subscribe: vi.fn(() => vi.fn()),
|
||||
},
|
||||
rootProps: {
|
||||
onPointerDown: vi.fn(),
|
||||
onPointerMove: vi.fn(),
|
||||
onPointerLeave: vi.fn(),
|
||||
},
|
||||
thumbProps: {
|
||||
onKeyDown: vi.fn(),
|
||||
onFocus: vi.fn(),
|
||||
onBlur: vi.fn(),
|
||||
},
|
||||
adjustForAlignment: <S,>(state: S): S => state,
|
||||
destroy: vi.fn(),
|
||||
}),
|
||||
mockVolumeState: volumeState,
|
||||
// Mutable holder so tests can swap between null and available volume.
|
||||
mutableVolume: { current: volumeState as typeof volumeState | null },
|
||||
};
|
||||
});
|
||||
|
||||
// --- Module mocks ---
|
||||
|
||||
@@ -56,21 +62,28 @@ vi.mock('@videojs/store/react', () => ({
|
||||
useSnapshot: vi.fn((state: { current: unknown }) => state.current),
|
||||
useStore: vi.fn((_store: unknown, selector?: (state: object) => unknown) => {
|
||||
if (!selector) return _store;
|
||||
|
||||
// Return the mutable volume state directly for volume selectors.
|
||||
const vol = mutableVolume.current;
|
||||
if (!vol) return undefined;
|
||||
|
||||
try {
|
||||
const result = selector({ volume: mockVolumeState });
|
||||
const result = selector(vol);
|
||||
if (result !== undefined) return result;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
try {
|
||||
return selector(mockVolumeState);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}),
|
||||
}));
|
||||
|
||||
afterEach(cleanup);
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
mutableVolume.current = mockVolumeState;
|
||||
mockVolumeState.setVolume.mockClear();
|
||||
mockVolumeState.toggleMuted.mockClear();
|
||||
});
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
@@ -188,3 +201,92 @@ describe('VolumeSlider compound', () => {
|
||||
expect(output?.textContent).toContain('%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('VolumeSliderRoot wheel handling', () => {
|
||||
it('attaches a non-passive wheel listener on the root element', () => {
|
||||
// Capture the raw options before jsdom normalizes them.
|
||||
const capturedOptions: AddEventListenerOptions[] = [];
|
||||
const origAdd = HTMLDivElement.prototype.addEventListener;
|
||||
const addSpy = vi.fn(function (
|
||||
this: HTMLDivElement,
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
) {
|
||||
if (type === 'wheel' && typeof options === 'object') {
|
||||
capturedOptions.push({ ...options });
|
||||
}
|
||||
return origAdd.call(this, type, listener, options as AddEventListenerOptions);
|
||||
});
|
||||
HTMLDivElement.prototype.addEventListener = addSpy as typeof origAdd;
|
||||
|
||||
const { Wrapper } = createPlayerWrapper();
|
||||
render(
|
||||
<Wrapper>
|
||||
<VolumeSliderRoot />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
HTMLDivElement.prototype.addEventListener = origAdd;
|
||||
|
||||
expect(capturedOptions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(capturedOptions.some((opts) => opts.passive === false)).toBe(true);
|
||||
});
|
||||
|
||||
it('honors disabled prop changes after initial render', () => {
|
||||
const { Wrapper } = createPlayerWrapper();
|
||||
|
||||
// Render with disabled=true, dispatch wheel — setVolume should not be called.
|
||||
const { container, rerender } = render(
|
||||
<Wrapper>
|
||||
<VolumeSliderRoot disabled />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
const el = container.querySelector('[data-orientation]') as HTMLElement;
|
||||
expect(el).toBeTruthy();
|
||||
|
||||
el.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true }));
|
||||
expect(mockVolumeState.setVolume).not.toHaveBeenCalled();
|
||||
|
||||
// Rerender with disabled=false, dispatch wheel — setVolume should be called.
|
||||
rerender(
|
||||
<Wrapper>
|
||||
<VolumeSliderRoot disabled={false} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
el.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true }));
|
||||
expect(mockVolumeState.setVolume).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('attaches wheel handling when volume appears after initial null', () => {
|
||||
// Start with no volume — component returns null.
|
||||
mutableVolume.current = null;
|
||||
|
||||
const { Wrapper } = createPlayerWrapper();
|
||||
const { container, rerender } = render(
|
||||
<Wrapper>
|
||||
<VolumeSliderRoot />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
// Should not render when volume is null.
|
||||
expect(container.querySelector('[data-orientation]')).toBeNull();
|
||||
|
||||
// Simulate volume becoming available.
|
||||
mutableVolume.current = mockVolumeState;
|
||||
rerender(
|
||||
<Wrapper>
|
||||
<VolumeSliderRoot />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
const el = container.querySelector('[data-orientation]') as HTMLElement;
|
||||
expect(el).toBeTruthy();
|
||||
|
||||
// Wheel on the newly mounted root should call setVolume.
|
||||
el.dispatchEvent(new WheelEvent('wheel', { deltaY: -120, bubbles: true }));
|
||||
expect(mockVolumeState.setVolume).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { VolumeSliderCore, VolumeSliderDataAttrs } from '@videojs/core';
|
||||
import { getSliderCSSVars, logMissingFeature, selectVolume } from '@videojs/core/dom';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { createWheelStep, getSliderCSSVars, logMissingFeature, selectVolume } from '@videojs/core/dom';
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
import { forwardRef, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { usePlayer } from '../../player/context';
|
||||
import type { UIComponentProps } from '../../utils/types';
|
||||
@@ -34,6 +35,7 @@ export const VolumeSliderRoot = forwardRef<HTMLDivElement, VolumeSliderRootProps
|
||||
orientation,
|
||||
step = VolumeSliderCore.defaultProps.step,
|
||||
largeStep = VolumeSliderCore.defaultProps.largeStep,
|
||||
wheelStep = VolumeSliderCore.defaultProps.wheelStep,
|
||||
disabled,
|
||||
thumbAlignment,
|
||||
onDragStart,
|
||||
@@ -44,10 +46,15 @@ export const VolumeSliderRoot = forwardRef<HTMLDivElement, VolumeSliderRootProps
|
||||
const volume = usePlayer(selectVolume);
|
||||
|
||||
const [core] = useState(() => new VolumeSliderCore());
|
||||
core.setProps({ label, orientation, step, largeStep, disabled, thumbAlignment });
|
||||
core.setProps({ label, orientation, step, largeStep, wheelStep, disabled, thumbAlignment });
|
||||
|
||||
// Keep a ref to the latest volume state for callbacks.
|
||||
// Keep refs to the latest dynamic values for stable closures.
|
||||
const volumeRef = useLatestRef(volume);
|
||||
const disabledRef = useLatestRef(disabled);
|
||||
|
||||
const getPercent = () => (volumeRef.current?.volume ?? 0) * 100;
|
||||
const getStepPercent = () => core.getStepPercent();
|
||||
const setVolume = (percent: number) => volumeRef.current?.setVolume(percent / 100);
|
||||
|
||||
const { state, cssVars, rootRef, thumbRef, rootProps, rootStyle, thumbProps } = useSlider<VolumeSliderCore.State>({
|
||||
computeState: (input) => {
|
||||
@@ -55,24 +62,43 @@ export const VolumeSliderRoot = forwardRef<HTMLDivElement, VolumeSliderRootProps
|
||||
core.setMedia(volume ?? noopVolume);
|
||||
return core.getState();
|
||||
},
|
||||
getPercent: () => (volume ? volume.volume * 100 : 0),
|
||||
getStepPercent: () => core.getStepPercent(),
|
||||
getPercent,
|
||||
getStepPercent,
|
||||
getLargeStepPercent: () => core.getLargeStepPercent(),
|
||||
orientation,
|
||||
disabled,
|
||||
adjustPercent: (rawPercent, thumbSize, trackSize) =>
|
||||
core.adjustPercentForAlignment(rawPercent, thumbSize, trackSize),
|
||||
getCSSVars: getSliderCSSVars,
|
||||
onValueChange: (percent) => {
|
||||
volumeRef.current?.setVolume(percent / 100);
|
||||
},
|
||||
onValueCommit: (percent) => {
|
||||
volumeRef.current?.setVolume(percent / 100);
|
||||
},
|
||||
onValueChange: setVolume,
|
||||
onValueCommit: setVolume,
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
});
|
||||
|
||||
const [wheelHandler] = useState(() =>
|
||||
createWheelStep({
|
||||
isDisabled: () => !!disabledRef.current || !volumeRef.current,
|
||||
getPercent: () => (volumeRef.current?.volume ?? 0) * 100,
|
||||
getStepPercent: () => core.getWheelStepPercent(),
|
||||
onValueChange: (percent) => volumeRef.current?.setVolume(percent / 100),
|
||||
})
|
||||
);
|
||||
|
||||
// Attach non-passive wheel listener via callback ref so it covers
|
||||
// late-mounted elements (null → mounted after volume appears).
|
||||
const wheelCleanupRef = useRef<(() => void) | null>(null);
|
||||
const wheelRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
wheelCleanupRef.current?.();
|
||||
wheelCleanupRef.current = null;
|
||||
if (element) {
|
||||
wheelCleanupRef.current = listen(element, 'wheel', wheelHandler.onWheel, { passive: false });
|
||||
}
|
||||
},
|
||||
[wheelHandler]
|
||||
);
|
||||
|
||||
if (!volume) {
|
||||
if (__DEV__) logMissingFeature('VolumeSlider', 'volume');
|
||||
return null;
|
||||
@@ -96,7 +122,7 @@ export const VolumeSliderRoot = forwardRef<HTMLDivElement, VolumeSliderRootProps
|
||||
{
|
||||
state,
|
||||
stateAttrMap: VolumeSliderDataAttrs,
|
||||
ref: [forwardedRef, rootRef],
|
||||
ref: [forwardedRef, rootRef, wheelRef],
|
||||
props: [{ style: { ...cssVars, ...rootStyle } }, rootProps, elementProps],
|
||||
}
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user