refactor(packages): dry up core, html, and react UI architecture (#699)

This commit is contained in:
rahim
2026-03-03 23:27:53 -08:00
committed by GitHub
parent 8bdd4ce7fa
commit 1edeadefed
110 changed files with 1349 additions and 1359 deletions
+3 -1
View File
@@ -17,8 +17,10 @@ export { BufferingIndicatorElement } from './ui/buffering-indicator/buffering-in
export { ControlsElement } from './ui/controls/controls-element';
export { ControlsGroupElement } from './ui/controls/controls-group-element';
export { FullscreenButtonElement } from './ui/fullscreen-button/fullscreen-button-element';
export { MediaButtonElement } from './ui/media-button-element';
// Primitives
export * from './ui/media-element';
export { MediaUIElement } from './ui/media-ui-element';
export { MuteButtonElement } from './ui/mute-button/mute-button-element';
export { PiPButtonElement } from './ui/pip-button/pip-button-element';
export { PlayButtonElement } from './ui/play-button/play-button-element';
@@ -26,8 +28,8 @@ export { PlaybackRateButtonElement } from './ui/playback-rate-button/playback-ra
export { PopoverElement } from './ui/popover/popover-element';
export { PosterElement } from './ui/poster/poster-element';
export { SeekButtonElement } from './ui/seek-button/seek-button-element';
export { type SliderContextValue, sliderContext } from './ui/slider/context';
export { SliderBufferElement } from './ui/slider/slider-buffer-element';
export { type SliderContextValue, sliderContext } from './ui/slider/slider-context';
export { SliderElement } from './ui/slider/slider-element';
export type { SliderEventMap, SliderValueEventDetail } from './ui/slider/slider-events';
export { SliderFillElement } from './ui/slider/slider-fill-element';
@@ -83,6 +83,10 @@ export class PlayerController<Store extends PlayerStore, Result = Store> impleme
return this.#store?.value;
}
get displayName(): string | undefined {
return this.#selector?.displayName;
}
hostConnected(): void {
const store = this.#consumer.value;
if (store) this.#connect(store);
@@ -30,7 +30,7 @@ export class BufferingIndicatorElement extends MediaElement {
});
if (__DEV__ && !this.#state.value) {
logMissingFeature(BufferingIndicatorElement.tagName, 'playback');
logMissingFeature(this.localName, this.#state.displayName!);
}
}
@@ -0,0 +1,34 @@
import type { StateAttrMap } from '@videojs/core';
import { applyStateDataAttrs } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import { MediaElement } from './media-element';
/** Shape that compound context values must satisfy for parts to consume. */
export interface PartContextValue<State extends object> {
state: State;
stateAttrMap: StateAttrMap<State>;
}
/**
* Abstract base for compound-component part elements that consume a parent
* context and apply data attributes from `ctx.state` + `ctx.stateAttrMap`.
*
* Subclasses only need to declare the `consumer` property:
*
* ```ts
* export class SliderTrackElement extends ContextPartElement<SliderState> {
* static readonly tagName = 'media-slider-track';
* protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
* }
* ```
*/
export abstract class ContextPartElement<State extends object> extends MediaElement {
protected abstract readonly consumer: { value?: PartContextValue<State> | undefined };
protected override update(_changed: PropertyValues): void {
super.update(_changed);
const ctx = this.consumer.value;
if (ctx) applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
}
}
+11
View File
@@ -0,0 +1,11 @@
import type { ControlsState, StateAttrMap } from '@videojs/core';
import { createContext } from '@videojs/element/context';
export interface ControlsContextValue {
state: ControlsState;
stateAttrMap: StateAttrMap<ControlsState>;
}
const CONTROLS_CONTEXT_KEY = Symbol('@videojs/controls');
export const controlsContext = createContext<ControlsContextValue>(CONTROLS_CONTEXT_KEY);
@@ -1,34 +1,42 @@
import { ControlsCore, ControlsDataAttrs } from '@videojs/core';
import { applyStateDataAttrs, logMissingFeature, selectControls } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import { ContextProvider } from '@videojs/element/context';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { controlsContext } from './context';
export class ControlsElement extends MediaElement {
static readonly tagName = 'media-controls';
readonly #core = new ControlsCore();
readonly #state = new PlayerController(this, playerContext, selectControls);
readonly #mediaState = new PlayerController(this, playerContext, selectControls);
readonly #provider = new ContextProvider(this, { context: controlsContext });
override connectedCallback(): void {
super.connectedCallback();
if (__DEV__ && !this.#state.value) {
logMissingFeature(ControlsElement.tagName, 'controls');
if (__DEV__ && !this.#mediaState.value && this.#mediaState.displayName) {
logMissingFeature(this.localName, this.#mediaState.displayName);
}
}
protected override update(changed: PropertyValues): void {
super.update(changed);
protected override update(_changed: PropertyValues): void {
super.update(_changed);
const controls = this.#state.value;
const media = this.#mediaState.value;
if (!media) return;
if (!controls) {
return;
}
this.#core.setMedia(media);
const state = this.#core.getState();
applyStateDataAttrs(this, this.#core.getState(controls), ControlsDataAttrs);
applyStateDataAttrs(this, state, ControlsDataAttrs);
this.#provider.setValue({
state,
stateAttrMap: ControlsDataAttrs,
});
}
}
@@ -1,8 +1,14 @@
import { MediaElement } from '../media-element';
import type { ControlsState } from '@videojs/core';
import { ContextConsumer } from '@videojs/element/context';
export class ControlsGroupElement extends MediaElement {
import { ContextPartElement } from '../context-part-element';
import { controlsContext } from './context';
export class ControlsGroupElement extends ContextPartElement<ControlsState> {
static readonly tagName = 'media-controls-group';
protected readonly consumer = new ContextConsumer(this, { context: controlsContext, subscribe: true });
override connectedCallback(): void {
super.connectedCallback();
@@ -1,70 +1,18 @@
import { FullscreenButtonCore, FullscreenButtonDataAttrs } from '@videojs/core';
import {
applyElementProps,
applyStateDataAttrs,
createButton,
logMissingFeature,
selectFullscreen,
} from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { FullscreenButtonCore, FullscreenButtonDataAttrs, type MediaFullscreenState } from '@videojs/core';
import { selectFullscreen } from '@videojs/core/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { MediaButtonElement } from '../media-button-element';
export class FullscreenButtonElement extends MediaElement {
export class FullscreenButtonElement extends MediaButtonElement<FullscreenButtonCore> {
static readonly tagName = 'media-fullscreen-button';
static override properties = {
label: { type: String },
disabled: { type: Boolean },
} satisfies PropertyDeclarationMap<keyof FullscreenButtonCore.Props>;
protected readonly core = new FullscreenButtonCore();
protected readonly stateAttrMap = FullscreenButtonDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectFullscreen);
label = FullscreenButtonCore.defaultProps.label;
disabled = FullscreenButtonCore.defaultProps.disabled;
readonly #core = new FullscreenButtonCore();
readonly #state = new PlayerController(this, playerContext, selectFullscreen);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => this.#core.toggle(this.#state.value!),
isDisabled: () => this.disabled || !this.#state.value,
});
applyElementProps(this, buttonProps, this.#disconnect.signal);
if (__DEV__ && !this.#state.value) {
logMissingFeature(FullscreenButtonElement.tagName, 'fullscreen');
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override willUpdate(changed: PropertyValues): void {
super.willUpdate(changed);
this.#core.setProps(this);
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.#state.value;
if (!media) return;
const state = this.#core.getState(media);
applyElementProps(this, this.#core.getAttrs(state));
applyStateDataAttrs(this, state, FullscreenButtonDataAttrs);
protected activate(state: MediaFullscreenState): void {
this.core.toggle(state);
}
}
@@ -0,0 +1,66 @@
import type { InferComponentState, InferMediaState, MediaUIComponent, StateAttrMap } from '@videojs/core';
import { applyElementProps, applyStateDataAttrs, createButton, logMissingFeature } from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import type { PlayerController } from '../player/player-controller';
import { MediaElement } from './media-element';
/** Abstract base for HTML custom elements that render a media-control button. */
export abstract class MediaButtonElement<Core extends MediaUIComponent> extends MediaElement {
static override properties: PropertyDeclarationMap = {
label: { type: String },
disabled: { type: Boolean },
};
disabled = false;
label = '';
protected abstract readonly core: Core;
protected abstract readonly stateAttrMap: StateAttrMap<InferComponentState<Core>>;
protected abstract readonly mediaState: PlayerController<any, InferMediaState<Core> | undefined>;
protected abstract activate(state: InferMediaState<Core>): void;
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => this.activate(this.mediaState.value!),
isDisabled: () => this.disabled || !this.mediaState.value,
});
applyElementProps(this, buttonProps, { signal: this.#disconnect.signal });
if (__DEV__ && !this.mediaState.value && this.mediaState.displayName) {
logMissingFeature(this.localName, this.mediaState.displayName);
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override willUpdate(changed: PropertyValues): void {
super.willUpdate(changed);
this.core.setProps?.(this);
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.mediaState.value;
if (!media) return;
this.core.setMedia(media);
const state = this.core.getState();
applyElementProps(this, this.core.getAttrs?.(state) ?? {});
applyStateDataAttrs(this, state, this.stateAttrMap);
}
}
+33
View File
@@ -0,0 +1,33 @@
import type { InferComponentState, InferMediaState, MediaUIComponent, StateAttrMap } from '@videojs/core';
import { applyStateDataAttrs, logMissingFeature } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import type { PlayerController } from '../player/player-controller';
import { MediaElement } from './media-element';
/** Abstract base for HTML custom elements that display media state with data attributes. */
export abstract class MediaUIElement<Core extends MediaUIComponent> extends MediaElement {
protected abstract readonly core: Core;
protected abstract readonly stateAttrMap: StateAttrMap<InferComponentState<Core>>;
protected abstract readonly mediaState: PlayerController<any, InferMediaState<Core> | undefined>;
override connectedCallback(): void {
super.connectedCallback();
if (__DEV__ && !this.mediaState.value && this.mediaState.displayName) {
logMissingFeature(this.localName, this.mediaState.displayName);
}
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.mediaState.value;
if (!media) return;
this.core.setMedia(media);
const state = this.core.getState();
applyStateDataAttrs(this, state, this.stateAttrMap);
}
}
@@ -1,70 +1,18 @@
import { MuteButtonCore, MuteButtonDataAttrs } from '@videojs/core';
import {
applyElementProps,
applyStateDataAttrs,
createButton,
logMissingFeature,
selectVolume,
} from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { type MediaVolumeState, MuteButtonCore, MuteButtonDataAttrs } from '@videojs/core';
import { selectVolume } from '@videojs/core/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { MediaButtonElement } from '../media-button-element';
export class MuteButtonElement extends MediaElement {
export class MuteButtonElement extends MediaButtonElement<MuteButtonCore> {
static readonly tagName = 'media-mute-button';
static override properties = {
label: { type: String },
disabled: { type: Boolean },
} satisfies PropertyDeclarationMap<keyof MuteButtonCore.Props>;
protected readonly core = new MuteButtonCore();
protected readonly stateAttrMap = MuteButtonDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectVolume);
label = MuteButtonCore.defaultProps.label;
disabled = MuteButtonCore.defaultProps.disabled;
readonly #core = new MuteButtonCore();
readonly #state = new PlayerController(this, playerContext, selectVolume);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => this.#core.toggle(this.#state.value!),
isDisabled: () => this.disabled || !this.#state.value,
});
applyElementProps(this, buttonProps, this.#disconnect.signal);
if (__DEV__ && !this.#state.value) {
logMissingFeature(MuteButtonElement.tagName, 'volume');
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override willUpdate(changed: PropertyValues): void {
super.willUpdate(changed);
this.#core.setProps(this);
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.#state.value;
if (!media) return;
const state = this.#core.getState(media);
applyElementProps(this, this.#core.getAttrs(state));
applyStateDataAttrs(this, state, MuteButtonDataAttrs);
protected activate(state: MediaVolumeState): void {
this.core.toggle(state);
}
}
@@ -1,64 +1,18 @@
import { PiPButtonCore, PiPButtonDataAttrs } from '@videojs/core';
import { applyElementProps, applyStateDataAttrs, createButton, logMissingFeature, selectPiP } from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { type MediaPictureInPictureState, PiPButtonCore, PiPButtonDataAttrs } from '@videojs/core';
import { selectPiP } from '@videojs/core/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { MediaButtonElement } from '../media-button-element';
export class PiPButtonElement extends MediaElement {
export class PiPButtonElement extends MediaButtonElement<PiPButtonCore> {
static readonly tagName = 'media-pip-button';
static override properties = {
label: { type: String },
disabled: { type: Boolean },
} satisfies PropertyDeclarationMap<keyof PiPButtonCore.Props>;
protected readonly core = new PiPButtonCore();
protected readonly stateAttrMap = PiPButtonDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectPiP);
label = PiPButtonCore.defaultProps.label;
disabled = PiPButtonCore.defaultProps.disabled;
readonly #core = new PiPButtonCore();
readonly #state = new PlayerController(this, playerContext, selectPiP);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => this.#core.toggle(this.#state.value!),
isDisabled: () => this.disabled || !this.#state.value,
});
applyElementProps(this, buttonProps, this.#disconnect.signal);
if (__DEV__ && !this.#state.value) {
logMissingFeature(PiPButtonElement.tagName, 'pip');
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override willUpdate(changed: PropertyValues): void {
super.willUpdate(changed);
this.#core.setProps(this);
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.#state.value;
if (!media) return;
const state = this.#core.getState(media);
applyElementProps(this, this.#core.getAttrs(state));
applyStateDataAttrs(this, state, PiPButtonDataAttrs);
protected activate(state: MediaPictureInPictureState): void {
this.core.toggle(state);
}
}
@@ -1,70 +1,18 @@
import { PlayButtonCore, PlayButtonDataAttrs } from '@videojs/core';
import {
applyElementProps,
applyStateDataAttrs,
createButton,
logMissingFeature,
selectPlayback,
} from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { type MediaPlaybackState, PlayButtonCore, PlayButtonDataAttrs } from '@videojs/core';
import { selectPlayback } from '@videojs/core/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { MediaButtonElement } from '../media-button-element';
export class PlayButtonElement extends MediaElement {
export class PlayButtonElement extends MediaButtonElement<PlayButtonCore> {
static readonly tagName = 'media-play-button';
static override properties = {
label: { type: String },
disabled: { type: Boolean },
} satisfies PropertyDeclarationMap<keyof PlayButtonCore.Props>;
protected readonly core = new PlayButtonCore();
protected readonly stateAttrMap = PlayButtonDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectPlayback);
label = PlayButtonCore.defaultProps.label;
disabled = PlayButtonCore.defaultProps.disabled;
readonly #core = new PlayButtonCore();
readonly #state = new PlayerController(this, playerContext, selectPlayback);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => this.#core.toggle(this.#state.value!),
isDisabled: () => this.disabled || !this.#state.value,
});
applyElementProps(this, buttonProps, this.#disconnect.signal);
if (__DEV__ && !this.#state.value) {
logMissingFeature(PlayButtonElement.tagName, 'playback');
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override willUpdate(changed: PropertyValues): void {
super.willUpdate(changed);
this.#core.setProps(this);
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.#state.value;
if (!media) return;
const state = this.#core.getState(media);
applyElementProps(this, this.#core.getAttrs(state));
applyStateDataAttrs(this, state, PlayButtonDataAttrs);
protected activate(state: MediaPlaybackState): void {
this.core.toggle(state);
}
}
@@ -1,70 +1,18 @@
import { PlaybackRateButtonCore, PlaybackRateButtonDataAttrs } from '@videojs/core';
import {
applyElementProps,
applyStateDataAttrs,
createButton,
logMissingFeature,
selectPlaybackRate,
} from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { type MediaPlaybackRateState, PlaybackRateButtonCore, PlaybackRateButtonDataAttrs } from '@videojs/core';
import { selectPlaybackRate } from '@videojs/core/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { MediaButtonElement } from '../media-button-element';
export class PlaybackRateButtonElement extends MediaElement {
export class PlaybackRateButtonElement extends MediaButtonElement<PlaybackRateButtonCore> {
static readonly tagName = 'media-playback-rate-button';
static override properties = {
label: { type: String },
disabled: { type: Boolean },
} satisfies PropertyDeclarationMap<keyof PlaybackRateButtonCore.Props>;
protected readonly core = new PlaybackRateButtonCore();
protected readonly stateAttrMap = PlaybackRateButtonDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectPlaybackRate);
label = PlaybackRateButtonCore.defaultProps.label;
disabled = PlaybackRateButtonCore.defaultProps.disabled;
readonly #core = new PlaybackRateButtonCore();
readonly #state = new PlayerController(this, playerContext, selectPlaybackRate);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => this.#core.cycle(this.#state.value!),
isDisabled: () => this.disabled || !this.#state.value,
});
applyElementProps(this, buttonProps, this.#disconnect.signal);
if (__DEV__ && !this.#state.value) {
logMissingFeature(PlaybackRateButtonElement.tagName, 'playbackRate');
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override willUpdate(changed: PropertyValues): void {
super.willUpdate(changed);
this.#core.setProps(this);
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.#state.value;
if (!media) return;
const state = this.#core.getState(media);
applyElementProps(this, this.#core.getAttrs(state));
applyStateDataAttrs(this, state, PlaybackRateButtonDataAttrs);
protected activate(state: MediaPlaybackRateState): void {
this.core.cycle(state);
}
}
+19 -18
View File
@@ -1,13 +1,13 @@
import { PopoverCore, PopoverDataAttrs, type PopoverInteraction, type PopoverProps } from '@videojs/core';
import { PopoverCore, PopoverDataAttrs, type PopoverInput, type PopoverProps } from '@videojs/core';
import {
applyElementProps,
applyStateDataAttrs,
createPopover,
createTransitionHandler,
createTransition,
getAnchorNameStyle,
getAnchorPositionStyle,
type PopoverApi,
type PopoverChangeDetails,
type PopoverHandle,
resolveOffsets,
} from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
@@ -44,12 +44,12 @@ export class PopoverElement extends MediaElement {
closeDelay = PopoverCore.defaultProps.closeDelay;
readonly #core = new PopoverCore();
#popover: PopoverHandle | null = null;
#snapshot: SnapshotController<PopoverInteraction> | null = null;
#popover: PopoverApi | null = null;
#snapshot: SnapshotController<PopoverInput> | null = null;
// Cleanup controllers
#disconnect: AbortController | null = null;
#triggerAc: AbortController | null = null;
#triggerAbort: AbortController | null = null;
#currentTrigger: HTMLElement | null = null;
override connectedCallback(): void {
@@ -57,7 +57,7 @@ export class PopoverElement extends MediaElement {
this.#disconnect = new AbortController();
this.#popover = createPopover({
transition: createTransitionHandler(),
transition: createTransition(),
onOpenChange: (nextOpen: boolean, details: PopoverChangeDetails) => {
this.open = nextOpen;
this.dispatchEvent(new CustomEvent('open-change', { detail: { open: nextOpen, ...details } }));
@@ -73,15 +73,15 @@ export class PopoverElement extends MediaElement {
this.#popover.setPopupElement(this);
// Apply popup event handlers (pointerenter/leave, focusout) to self.
applyElementProps(this, this.#popover.popupProps, this.#disconnect.signal);
applyElementProps(this, this.#popover.popupProps, { signal: this.#disconnect.signal });
// Subscribe to interaction state for reactive updates.
// Reuse the controller across connect/disconnect cycles to avoid
// leaking stale controllers in the host's controller set.
if (this.#snapshot) {
this.#snapshot.track(this.#popover.interaction);
this.#snapshot.track(this.#popover.input);
} else {
this.#snapshot = new SnapshotController(this, this.#popover.interaction);
this.#snapshot = new SnapshotController(this, this.#popover.input);
}
}
@@ -110,7 +110,7 @@ export class PopoverElement extends MediaElement {
// Sync controlled open state
if (this.#popover && changed.has('open')) {
const { active: interactionOpen } = this.#popover.interaction.current;
const { active: interactionOpen } = this.#popover.input.current;
if (this.open !== interactionOpen) {
if (this.open) {
this.#popover.open();
@@ -129,9 +129,10 @@ export class PopoverElement extends MediaElement {
const triggerEl = this.#findTrigger();
this.#syncTrigger(triggerEl);
// Derive state from core + interaction.
const interaction = this.#popover.interaction.current;
const state = this.#core.getState(interaction);
// Derive state from core + input.
const input = this.#popover.input.current;
this.#core.setInput(input);
const state = this.#core.getState();
// Apply popup ARIA and data attributes to self.
applyElementProps(this, this.#core.getPopupAttrs(state));
@@ -178,8 +179,8 @@ export class PopoverElement extends MediaElement {
this.#popover?.setTriggerElement(triggerEl);
if (triggerEl && this.#popover) {
this.#triggerAc = new AbortController();
applyElementProps(triggerEl, this.#popover.triggerProps, this.#triggerAc.signal);
this.#triggerAbort = new AbortController();
applyElementProps(triggerEl, this.#popover.triggerProps, { signal: this.#triggerAbort.signal });
}
}
@@ -194,8 +195,8 @@ export class PopoverElement extends MediaElement {
this.#currentTrigger.style.removeProperty('anchor-name');
}
this.#triggerAc?.abort();
this.#triggerAc = null;
this.#triggerAbort?.abort();
this.#triggerAbort = null;
this.#currentTrigger = null;
}
}
+6 -26
View File
@@ -1,34 +1,14 @@
import { PosterCore, PosterDataAttrs } from '@videojs/core';
import { applyStateDataAttrs, logMissingFeature, selectPlayback } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import { selectPlayback } from '@videojs/core/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { MediaUIElement } from '../media-ui-element';
export class PosterElement extends MediaElement {
export class PosterElement extends MediaUIElement<PosterCore> {
static readonly tagName = 'media-poster';
readonly #core = new PosterCore();
readonly #state = new PlayerController(this, playerContext, selectPlayback);
override connectedCallback(): void {
super.connectedCallback();
if (__DEV__ && !this.#state.value) {
logMissingFeature(PosterElement.tagName, 'playback');
}
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.#state.value;
if (!media) {
return;
}
applyStateDataAttrs(this, this.#core.getState(media), PosterDataAttrs);
}
protected readonly core = new PosterCore();
protected readonly stateAttrMap = PosterDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectPlayback);
}
@@ -1,66 +1,26 @@
import { SeekButtonCore, SeekButtonDataAttrs } from '@videojs/core';
import { applyElementProps, applyStateDataAttrs, createButton, logMissingFeature, selectTime } from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { type MediaTimeState, SeekButtonCore, SeekButtonDataAttrs } from '@videojs/core';
import { selectTime } from '@videojs/core/dom';
import type { PropertyDeclarationMap } from '@videojs/element';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { MediaButtonElement } from '../media-button-element';
export class SeekButtonElement extends MediaElement {
export class SeekButtonElement extends MediaButtonElement<SeekButtonCore> {
static readonly tagName = 'media-seek-button';
static override properties = {
static override properties: PropertyDeclarationMap = {
...MediaButtonElement.properties,
seconds: { type: Number },
label: { type: String },
disabled: { type: Boolean },
} satisfies PropertyDeclarationMap<keyof SeekButtonCore.Props>;
};
seconds = SeekButtonCore.defaultProps.seconds;
label = SeekButtonCore.defaultProps.label;
disabled = SeekButtonCore.defaultProps.disabled;
readonly #core = new SeekButtonCore();
readonly #state = new PlayerController(this, playerContext, selectTime);
protected readonly core = new SeekButtonCore();
protected readonly stateAttrMap = SeekButtonDataAttrs;
protected readonly mediaState = new PlayerController(this, playerContext, selectTime);
#disconnect: AbortController | null = null;
override connectedCallback(): void {
super.connectedCallback();
this.#disconnect = new AbortController();
const buttonProps = createButton({
onActivate: () => this.#core.seek(this.#state.value!),
isDisabled: () => this.disabled || !this.#state.value,
});
applyElementProps(this, buttonProps, this.#disconnect.signal);
if (__DEV__ && !this.#state.value) {
logMissingFeature(SeekButtonElement.tagName, 'time');
}
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#disconnect?.abort();
this.#disconnect = null;
}
protected override willUpdate(changed: PropertyValues): void {
super.willUpdate(changed);
this.#core.setProps(this);
}
protected override update(changed: PropertyValues): void {
super.update(changed);
const media = this.#state.value;
if (!media) return;
const state = this.#core.getState(media);
applyElementProps(this, this.#core.getAttrs(state));
applyStateDataAttrs(this, state, SeekButtonDataAttrs);
protected activate(state: MediaTimeState): void {
this.core.seek(state);
}
}
@@ -1,10 +1,12 @@
import type { SliderState } from '@videojs/core';
import type { SliderState, StateAttrMap } from '@videojs/core';
import type { SliderThumbProps } from '@videojs/core/dom';
import { createContext } from '@videojs/element/context';
export interface SliderContextValue {
/** Base slider state — children use this for data attributes and value display. */
state: SliderState;
/** Maps state keys to `data-*` attribute names for part elements. */
stateAttrMap: StateAttrMap<SliderState>;
/** Domain value at the current pointer position (e.g., seconds for time, percent for volume). */
pointerValue: number;
/** ARIA attributes for the thumb element (role, tabindex, aria-value*, etc.). */
@@ -1,22 +1,11 @@
import { SliderDataAttrs } from '@videojs/core';
import { applyStateDataAttrs } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import type { SliderState } from '@videojs/core';
import { ContextConsumer } from '@videojs/element/context';
import { MediaElement } from '../media-element';
import { sliderContext } from './slider-context';
import { ContextPartElement } from '../context-part-element';
import { sliderContext } from './context';
export class SliderBufferElement extends MediaElement {
export class SliderBufferElement extends ContextPartElement<SliderState> {
static readonly tagName = 'media-slider-buffer';
readonly #ctx = new ContextConsumer(this, {
context: sliderContext,
subscribe: true,
});
protected override update(_changed: PropertyValues): void {
super.update(_changed);
const ctx = this.#ctx.value;
if (ctx) applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
}
protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
}
+9 -16
View File
@@ -1,11 +1,11 @@
import { SliderCore, SliderDataAttrs } from '@videojs/core';
import { applyStateDataAttrs, createSlider, getSliderCSSVars, type SliderHandle } from '@videojs/core/dom';
import { applyStateDataAttrs, createSlider, getSliderCSSVars, type SliderApi } from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { ContextProvider } from '@videojs/element/context';
import { applyStyles, isRTL } from '@videojs/utils/dom';
import { MediaElement } from '../media-element';
import { sliderContext } from './slider-context';
import { sliderContext } from './context';
export class SliderElement extends MediaElement {
static readonly tagName = 'media-slider';
@@ -35,7 +35,7 @@ export class SliderElement extends MediaElement {
readonly #core = new SliderCore();
readonly #provider = new ContextProvider(this, { context: sliderContext });
#slider: SliderHandle | null = null;
#slider: SliderApi | null = null;
#disconnect: AbortController | null = null;
override connectedCallback(): void {
@@ -51,16 +51,8 @@ export class SliderElement extends MediaElement {
isRTL: () => isRTL(this),
isDisabled: () => this.disabled,
getPercent: () => this.#core.percentFromValue(this.value),
getStepPercent: () => {
const { step, min, max } = this.#core.props;
const range = max - min;
return range > 0 ? (step / range) * 100 : 0;
},
getLargeStepPercent: () => {
const { largeStep, min, max } = this.#core.props;
const range = max - min;
return range > 0 ? (largeStep / range) * 100 : 0;
},
getStepPercent: () => this.#core.getStepPercent(),
getLargeStepPercent: () => this.#core.getLargeStepPercent(),
onValueChange: (percent) => {
this.value = this.#core.valueFromPercent(percent);
this.dispatchEvent(new CustomEvent('value-change', { detail: { value: this.value }, bubbles: true }));
@@ -77,7 +69,7 @@ export class SliderElement extends MediaElement {
},
});
this.#slider.interaction.subscribe(() => this.requestUpdate(), { signal });
this.#slider.input.subscribe(() => this.requestUpdate(), { signal });
// Prevent default touch gestures and text selection during interaction.
this.style.touchAction = 'none';
@@ -101,8 +93,8 @@ export class SliderElement extends MediaElement {
super.update(_changed);
if (!this.#slider) return;
const interaction = this.#slider.interaction.current;
const state = this.#core.getState(interaction, this.value);
this.#core.setInput(this.#slider.input.current);
const state = this.#core.getSliderState(this.value);
const cssVars = getSliderCSSVars(state);
applyStyles(this, cssVars);
@@ -113,6 +105,7 @@ export class SliderElement extends MediaElement {
// Provide context to child elements (thumb, value, track, etc.).
this.#provider.setValue({
state,
stateAttrMap: SliderDataAttrs,
pointerValue: this.#core.valueFromPercent(state.pointerPercent),
thumbAttrs: this.#core.getAttrs(state),
thumbProps: this.#slider.thumbProps,
@@ -1,22 +1,11 @@
import { SliderDataAttrs } from '@videojs/core';
import { applyStateDataAttrs } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import type { SliderState } from '@videojs/core';
import { ContextConsumer } from '@videojs/element/context';
import { MediaElement } from '../media-element';
import { sliderContext } from './slider-context';
import { ContextPartElement } from '../context-part-element';
import { sliderContext } from './context';
export class SliderFillElement extends MediaElement {
export class SliderFillElement extends ContextPartElement<SliderState> {
static readonly tagName = 'media-slider-fill';
readonly #ctx = new ContextConsumer(this, {
context: sliderContext,
subscribe: true,
});
protected override update(_changed: PropertyValues): void {
super.update(_changed);
const ctx = this.#ctx.value;
if (ctx) applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
}
protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
}
@@ -1,10 +1,9 @@
import { SliderDataAttrs } from '@videojs/core';
import { applyElementProps, applyStateDataAttrs } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import { ContextConsumer } from '@videojs/element/context';
import { MediaElement } from '../media-element';
import { sliderContext } from './slider-context';
import { sliderContext } from './context';
export class SliderThumbElement extends MediaElement {
static readonly tagName = 'media-slider-thumb';
@@ -38,7 +37,7 @@ export class SliderThumbElement extends MediaElement {
// Apply keyboard and focus handlers once — they don't change per slider instance.
if (!this.#thumbPropsApplied && this.#disconnect) {
applyElementProps(this, ctx.thumbProps, this.#disconnect.signal);
applyElementProps(this, ctx.thumbProps, { signal: this.#disconnect.signal });
this.#thumbPropsApplied = true;
}
@@ -46,6 +45,6 @@ export class SliderThumbElement extends MediaElement {
applyElementProps(this, ctx.thumbAttrs);
// Apply state data attributes.
applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
}
}
@@ -1,22 +1,11 @@
import { SliderDataAttrs } from '@videojs/core';
import { applyStateDataAttrs } from '@videojs/core/dom';
import type { PropertyValues } from '@videojs/element';
import type { SliderState } from '@videojs/core';
import { ContextConsumer } from '@videojs/element/context';
import { MediaElement } from '../media-element';
import { sliderContext } from './slider-context';
import { ContextPartElement } from '../context-part-element';
import { sliderContext } from './context';
export class SliderTrackElement extends MediaElement {
export class SliderTrackElement extends ContextPartElement<SliderState> {
static readonly tagName = 'media-slider-track';
readonly #ctx = new ContextConsumer(this, {
context: sliderContext,
subscribe: true,
});
protected override update(_changed: PropertyValues): void {
super.update(_changed);
const ctx = this.#ctx.value;
if (ctx) applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
}
protected readonly consumer = new ContextConsumer(this, { context: sliderContext, subscribe: true });
}
@@ -1,10 +1,9 @@
import { SliderDataAttrs } from '@videojs/core';
import { applyStateDataAttrs } from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { ContextConsumer } from '@videojs/element/context';
import { MediaElement } from '../media-element';
import { sliderContext } from './slider-context';
import { sliderContext } from './context';
export class SliderValueElement extends MediaElement {
static readonly tagName = 'media-slider-value';
@@ -35,6 +34,6 @@ export class SliderValueElement extends MediaElement {
this.textContent = ctx.formatValue ? ctx.formatValue(value, this.type) : String(Math.round(value));
applyStateDataAttrs(this, ctx.state, SliderDataAttrs);
applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
}
}
@@ -6,7 +6,7 @@ import {
type ThumbnailImage,
type ThumbnailResizeResult,
} from '@videojs/core';
import type { ThumbnailHandle } from '@videojs/core/dom';
import type { ThumbnailApi } from '@videojs/core/dom';
import { applyElementProps, applyStateDataAttrs, createThumbnail, selectTextTrack } from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
@@ -45,7 +45,7 @@ export class ThumbnailElement extends MediaElement {
#thumbnails: ThumbnailImage[] = [];
#externalThumbnails: ThumbnailImage[] | undefined;
#lastTextTrack: MediaTextTrackState | undefined;
#handle: ThumbnailHandle | null = null;
#api: ThumbnailApi | null = null;
constructor() {
super();
@@ -79,7 +79,7 @@ export class ThumbnailElement extends MediaElement {
override connectedCallback(): void {
super.connectedCallback();
this.#handle = createThumbnail({
this.#api = createThumbnail({
getContainer: () => this,
getImg: () => this.#img,
onStateChange: () => this.requestUpdate(),
@@ -88,8 +88,8 @@ export class ThumbnailElement extends MediaElement {
override disconnectedCallback(): void {
super.disconnectedCallback();
this.#handle?.destroy();
this.#handle = null;
this.#api?.destroy();
this.#api = null;
}
protected override update(changed: PropertyValues): void {
@@ -119,8 +119,8 @@ export class ThumbnailElement extends MediaElement {
fetchpriority: this.fetchPriority,
});
// Track src changes via the handle.
this.#handle?.updateSrc(thumbnail?.url);
// Track src changes via the thumbnail API.
this.#api?.updateSrc(thumbnail?.url);
if (!thumbnail) {
this.#img.removeAttribute('src');
@@ -137,14 +137,14 @@ export class ThumbnailElement extends MediaElement {
this.#img.src = thumbnail.url;
}
const handle = this.#handle;
const state = this.#core.getState(handle?.loading ?? false, handle?.error ?? false, thumbnail);
const api = this.#api;
const state = this.#core.getState(api?.loading ?? false, api?.error ?? false, thumbnail);
applyElementProps(this, this.#core.getAttrs(state));
applyStateDataAttrs(this, state, ThumbnailDataAttrs);
if (handle?.naturalWidth && handle.naturalHeight) {
const constraints = handle.readConstraints();
const result = this.#core.resize(thumbnail, handle.naturalWidth, handle.naturalHeight, constraints);
if (api?.naturalWidth && api.naturalHeight) {
const constraints = api.readConstraints();
const result = this.#core.resize(thumbnail, api.naturalWidth, api.naturalHeight, constraints);
if (result) {
this.#applyResize(result);
@@ -4,18 +4,19 @@ import {
createSlider,
getTimeSliderCSSVars,
logMissingFeature,
type SliderHandle,
type SliderApi,
selectBuffer,
selectTime,
} from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
import { ContextProvider } from '@videojs/element/context';
import { applyStyles, isRTL } from '@videojs/utils/dom';
import { formatTime } from '@videojs/utils/time';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { sliderContext } from '../slider/slider-context';
import { sliderContext } from '../slider/context';
export class TimeSliderElement extends MediaElement {
static readonly tagName = 'media-time-slider';
@@ -28,7 +29,7 @@ export class TimeSliderElement extends MediaElement {
orientation: { type: String },
disabled: { type: Boolean },
thumbAlignment: { type: String, attribute: 'thumb-alignment' },
} satisfies PropertyDeclarationMap<keyof TimeSliderCore.Props>;
} satisfies PropertyDeclarationMap<Exclude<keyof TimeSliderCore.Props, 'value' | 'min' | 'max'>>;
label = TimeSliderCore.defaultProps.label;
commitThrottle = TimeSliderCore.defaultProps.commitThrottle;
@@ -43,7 +44,7 @@ export class TimeSliderElement extends MediaElement {
readonly #timeState = new PlayerController(this, playerContext, selectTime);
readonly #bufferState = new PlayerController(this, playerContext, selectBuffer);
#slider: SliderHandle | null = null;
#slider: SliderApi | null = null;
#disconnect: AbortController | null = null;
override connectedCallback(): void {
@@ -63,21 +64,11 @@ export class TimeSliderElement extends MediaElement {
if (!media) return 0;
return this.#core.percentFromValue(media.currentTime);
},
getStepPercent: () => {
const { step, min, max } = this.#core.props;
const range = max - min;
return range > 0 ? (step / range) * 100 : 0;
},
getLargeStepPercent: () => {
const { largeStep, min, max } = this.#core.props;
const range = max - min;
return range > 0 ? (largeStep / range) * 100 : 0;
},
onValueChange: () => {
// Visual update only — CSS vars are refreshed in update().
},
getStepPercent: () => this.#core.getStepPercent(),
getLargeStepPercent: () => this.#core.getLargeStepPercent(),
onValueCommit: (percent) => {
this.#seek(percent);
const media = this.#timeState.value;
if (media) media.seek(this.#core.valueFromPercent(percent));
},
commitThrottle: this.commitThrottle,
onDragStart: () => {
@@ -88,14 +79,14 @@ export class TimeSliderElement extends MediaElement {
},
});
this.#slider.interaction.subscribe(() => this.requestUpdate(), { signal });
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(TimeSliderElement.tagName, 'time');
logMissingFeature(this.localName, this.#timeState.displayName!);
}
}
@@ -120,9 +111,10 @@ export class TimeSliderElement extends MediaElement {
const buffer = this.#bufferState.value;
if (!time) return;
const interaction = this.#slider.interaction.current;
this.#core.setInput(this.#slider.input.current);
const media = { ...time, ...(buffer ?? { buffered: [], seekable: [] }) };
const state = this.#core.getTimeState(media, interaction);
this.#core.setMedia(media);
const state = this.#core.getState();
const cssVars = getTimeSliderCSSVars(state);
applyStyles(this, cssVars);
@@ -133,24 +125,11 @@ export class TimeSliderElement extends MediaElement {
// Provide context to child elements with base slider data attrs.
this.#provider.setValue({
state,
stateAttrMap: TimeSliderDataAttrs,
pointerValue: this.#core.valueFromPercent(state.pointerPercent),
thumbAttrs: this.#core.getAttrs(state),
thumbProps: this.#slider.thumbProps,
formatValue: (value) => formatTime(value),
formatValue: (value) => formatTime(value, state.duration),
});
}
#seek(percent: number): void {
const media = this.#timeState.value;
if (!media) return;
const time = this.#core.valueFromPercent(percent);
media.seek(time);
}
}
function formatTime(seconds: number): string {
const total = Math.round(seconds);
const minutes = Math.floor(total / 60);
const secs = total % 60;
return `${minutes}:${secs.toString().padStart(2, '0')}`;
}
+3 -2
View File
@@ -39,7 +39,7 @@ export class TimeElement extends MediaElement {
super.connectedCallback();
if (__DEV__ && !this.#state.value) {
logMissingFeature(TimeElement.tagName, 'time');
logMissingFeature(this.localName, this.#state.displayName!);
}
}
@@ -55,7 +55,8 @@ export class TimeElement extends MediaElement {
if (!media) return;
const state = this.#core.getState(media);
this.#core.setMedia(media);
const state = this.#core.getState();
this.#signSpan.hidden = !state.negative;
this.#signSpan.textContent = state.negative ? this.negativeSign : '';
@@ -4,7 +4,7 @@ import {
createSlider,
getSliderCSSVars,
logMissingFeature,
type SliderHandle,
type SliderApi,
selectVolume,
} from '@videojs/core/dom';
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
@@ -14,7 +14,7 @@ import { applyStyles, isRTL } from '@videojs/utils/dom';
import { playerContext } from '../../player/context';
import { PlayerController } from '../../player/player-controller';
import { MediaElement } from '../media-element';
import { sliderContext } from '../slider/slider-context';
import { sliderContext } from '../slider/context';
export class VolumeSliderElement extends MediaElement {
static readonly tagName = 'media-volume-slider';
@@ -26,7 +26,7 @@ export class VolumeSliderElement extends MediaElement {
orientation: { type: String },
disabled: { type: Boolean },
thumbAlignment: { type: String, attribute: 'thumb-alignment' },
} satisfies PropertyDeclarationMap<keyof VolumeSliderCore.Props>;
} satisfies PropertyDeclarationMap<Exclude<keyof VolumeSliderCore.Props, 'value' | 'min' | 'max'>>;
label = VolumeSliderCore.defaultProps.label;
step = VolumeSliderCore.defaultProps.step;
@@ -39,7 +39,7 @@ export class VolumeSliderElement extends MediaElement {
readonly #provider = new ContextProvider(this, { context: sliderContext });
readonly #volumeState = new PlayerController(this, playerContext, selectVolume);
#slider: SliderHandle | null = null;
#slider: SliderApi | null = null;
#disconnect: AbortController | null = null;
override connectedCallback(): void {
@@ -59,16 +59,8 @@ export class VolumeSliderElement extends MediaElement {
if (!media) return 0;
return media.volume * 100;
},
getStepPercent: () => {
const { step, min, max } = this.#core.props;
const range = max - min;
return range > 0 ? (step / range) * 100 : 0;
},
getLargeStepPercent: () => {
const { largeStep, min, max } = this.#core.props;
const range = max - min;
return range > 0 ? (largeStep / range) * 100 : 0;
},
getStepPercent: () => this.#core.getStepPercent(),
getLargeStepPercent: () => this.#core.getLargeStepPercent(),
onValueChange: (percent) => {
this.#setVolume(percent);
},
@@ -83,14 +75,14 @@ export class VolumeSliderElement extends MediaElement {
},
});
this.#slider.interaction.subscribe(() => this.requestUpdate(), { signal });
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(VolumeSliderElement.tagName, 'volume');
logMissingFeature(this.localName, this.#volumeState.displayName!);
}
}
@@ -114,8 +106,9 @@ export class VolumeSliderElement extends MediaElement {
const media = this.#volumeState.value;
if (!media) return;
const interaction = this.#slider.interaction.current;
const state = this.#core.getVolumeState(media, interaction);
this.#core.setInput(this.#slider.input.current);
this.#core.setMedia(media);
const state = this.#core.getState();
const cssVars = getSliderCSSVars(state);
applyStyles(this, cssVars);
@@ -126,6 +119,7 @@ export class VolumeSliderElement extends MediaElement {
// Provide context to child elements.
this.#provider.setValue({
state,
stateAttrMap: SliderDataAttrs,
pointerValue: this.#core.valueFromPercent(state.pointerPercent),
thumbAttrs: this.#core.getAttrs(state),
thumbProps: this.#slider.thumbProps,