mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(packages): add UI support for gestures and hotkeys (#1388)
Co-authored-by: Rahim <rahim.alwer@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Rahim
Claude Opus 4.6
parent
6c81f2d190
commit
0620814a67
@@ -80,6 +80,7 @@ export class GestureElement extends MediaElement {
|
||||
region: this.region,
|
||||
disabled: this.disabled,
|
||||
action: this.action,
|
||||
value: this.value,
|
||||
};
|
||||
|
||||
if (this.type === 'doubletap') {
|
||||
|
||||
@@ -65,6 +65,7 @@ export class HotkeyElement extends MediaElement {
|
||||
this.#cleanup = createHotkey(container, {
|
||||
keys: this.keys,
|
||||
action,
|
||||
value,
|
||||
target: this.target,
|
||||
disabled: this.disabled,
|
||||
repeatable: !isHotkeyToggleAction(action),
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import {
|
||||
getRenderedIndicatorState,
|
||||
type IndicatorLifecycleState,
|
||||
type IndicatorVisibilityHandle,
|
||||
type InputActionEvent,
|
||||
isIndicatorPresent,
|
||||
type MediaSnapshot,
|
||||
} from '@videojs/core';
|
||||
import {
|
||||
getIndicatorVisibilityCoordinator,
|
||||
getMediaSnapshot,
|
||||
subscribeToInputActions,
|
||||
type TransitionApi,
|
||||
} from '@videojs/core/dom';
|
||||
import type { PropertyValues } from '@videojs/element';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
import type { State as StoreState } from '@videojs/store';
|
||||
|
||||
import { containerContext, playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
import type { LiveIndicator } from './live-indicator';
|
||||
|
||||
/** Shared imperative API for status / volume / seek indicator cores. */
|
||||
export interface InputIndicatorCoreApi<IndicatorState extends IndicatorLifecycleState> {
|
||||
readonly state: StoreState<IndicatorState>;
|
||||
destroy(): void;
|
||||
close(): void;
|
||||
processEvent(event: InputActionEvent, snapshot: MediaSnapshot): boolean;
|
||||
}
|
||||
|
||||
export abstract class InputIndicatorElement<IndicatorState extends IndicatorLifecycleState> extends MediaElement {
|
||||
protected abstract get core(): InputIndicatorCoreApi<IndicatorState>;
|
||||
protected abstract get transition(): TransitionApi;
|
||||
protected abstract get liveIndicator(): LiveIndicator<IndicatorState>;
|
||||
|
||||
protected abstract syncCoreProps(): void;
|
||||
|
||||
protected readonly player = new PlayerController(this, playerContext);
|
||||
protected readonly container = new ContextConsumer(this, {
|
||||
context: containerContext,
|
||||
callback: () => this.#reconnect(),
|
||||
subscribe: true,
|
||||
});
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
#inputActionUnsubscribe: (() => void) | null = null;
|
||||
#visibilityUnsubscribe: (() => void) | null = null;
|
||||
#visibilityHandle: IndicatorVisibilityHandle | null = null;
|
||||
#lastGeneration = 0;
|
||||
#snapshot: IndicatorState | null = null;
|
||||
|
||||
#getVisibilityHandle(): IndicatorVisibilityHandle {
|
||||
return (this.#visibilityHandle ??= { close: () => this.core.close() });
|
||||
}
|
||||
|
||||
#payloadSnapshot(): IndicatorState {
|
||||
return this.#snapshot ?? this.core.state.current;
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.destroyed) return;
|
||||
|
||||
this.#snapshot = this.core.state.current;
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
this.core.state.subscribe(() => this.requestUpdate(), { signal: this.#disconnect.signal });
|
||||
this.transition.state.subscribe(() => this.requestUpdate(), { signal: this.#disconnect.signal });
|
||||
this.hidden = true;
|
||||
this.#reconnect();
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#inputActionUnsubscribe?.();
|
||||
this.#visibilityUnsubscribe?.();
|
||||
this.#inputActionUnsubscribe = null;
|
||||
this.#visibilityUnsubscribe = null;
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
override destroyCallback(): void {
|
||||
this.#inputActionUnsubscribe?.();
|
||||
this.#visibilityUnsubscribe?.();
|
||||
this.core.destroy();
|
||||
this.transition.destroy();
|
||||
this.liveIndicator.remove();
|
||||
super.destroyCallback();
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.syncCoreProps();
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
this.#syncTransition();
|
||||
|
||||
const currentState = this.core.state.current;
|
||||
const transitionState = this.transition.state.current;
|
||||
const present = isIndicatorPresent(currentState, transitionState);
|
||||
|
||||
if (!present) {
|
||||
this.liveIndicator.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getRenderedIndicatorState(currentState, this.#payloadSnapshot(), transitionState);
|
||||
this.liveIndicator.render(state);
|
||||
}
|
||||
|
||||
#syncTransition(): void {
|
||||
const currentState = this.core.state.current;
|
||||
|
||||
if (currentState.open) {
|
||||
this.#snapshot = currentState;
|
||||
if (this.#lastGeneration !== currentState.generation) {
|
||||
this.#lastGeneration = currentState.generation;
|
||||
void this.transition.open();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { active, status } = this.transition.state.current;
|
||||
if (active && status !== 'ending') {
|
||||
void this.transition.close(this.liveIndicator.element);
|
||||
}
|
||||
}
|
||||
|
||||
#reconnect(): void {
|
||||
this.#inputActionUnsubscribe?.();
|
||||
this.#visibilityUnsubscribe?.();
|
||||
this.#inputActionUnsubscribe = null;
|
||||
this.#visibilityUnsubscribe = null;
|
||||
|
||||
const container = this.container.value?.container;
|
||||
if (!container) return;
|
||||
|
||||
const visibility = getIndicatorVisibilityCoordinator(container);
|
||||
const visibilityHandle = this.#getVisibilityHandle();
|
||||
this.#visibilityUnsubscribe = visibility.register(visibilityHandle);
|
||||
|
||||
this.#inputActionUnsubscribe = subscribeToInputActions(container, (event) => {
|
||||
if (this.core.processEvent(event, getMediaSnapshot(this.player.value))) {
|
||||
visibility.show(visibilityHandle);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { StateAttrMap } from '@videojs/core';
|
||||
import { applyStateDataAttrs } from '@videojs/core/dom';
|
||||
|
||||
export interface LiveIndicatorOptions<State extends object> {
|
||||
host: HTMLElement;
|
||||
dataAttrs: StateAttrMap<State>;
|
||||
render: (element: HTMLElement, state: State) => void;
|
||||
}
|
||||
|
||||
export class LiveIndicator<State extends object> {
|
||||
readonly #host: HTMLElement;
|
||||
readonly #dataAttrs: StateAttrMap<State>;
|
||||
readonly #render: (element: HTMLElement, state: State) => void;
|
||||
|
||||
constructor(options: LiveIndicatorOptions<State>) {
|
||||
this.#host = options.host;
|
||||
this.#dataAttrs = options.dataAttrs;
|
||||
this.#render = options.render;
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
return this.#host;
|
||||
}
|
||||
|
||||
render(state: State): HTMLElement {
|
||||
this.#host.hidden = false;
|
||||
applyStateDataAttrs(this.#host, state, this.#dataAttrs);
|
||||
this.#render(this.#host, state);
|
||||
|
||||
return this.#host;
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.#host.hidden = true;
|
||||
|
||||
for (const key in this.#dataAttrs) {
|
||||
const name = this.#dataAttrs[key];
|
||||
if (name) this.#host.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
getVolumeIndicatorDisplayValue,
|
||||
type VolumeIndicatorCore,
|
||||
VolumeIndicatorCSSVars,
|
||||
VolumeIndicatorDataAttrs,
|
||||
} from '@videojs/core';
|
||||
import { getIndicatorVisibilityCoordinator } from '@videojs/core/dom';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { SeekIndicatorElement } from '../../seek-indicator/seek-indicator-element';
|
||||
import { SeekIndicatorValueElement } from '../../seek-indicator/seek-indicator-value-element';
|
||||
import { StatusAnnouncerElement } from '../../status-announcer/status-announcer-element';
|
||||
import { StatusIndicatorElement } from '../../status-indicator/status-indicator-element';
|
||||
import { StatusIndicatorValueElement } from '../../status-indicator/status-indicator-value-element';
|
||||
import { VolumeIndicatorElement } from '../../volume-indicator/volume-indicator-element';
|
||||
import { VolumeIndicatorFillElement } from '../../volume-indicator/volume-indicator-fill-element';
|
||||
import { VolumeIndicatorValueElement } from '../../volume-indicator/volume-indicator-value-element';
|
||||
import { LiveIndicator } from '../live-indicator';
|
||||
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
});
|
||||
|
||||
describe('input indicators', () => {
|
||||
it('exposes standalone indicator tag names', () => {
|
||||
expect(StatusIndicatorElement.tagName).toBe('media-status-indicator');
|
||||
expect(StatusIndicatorValueElement.tagName).toBe('media-status-indicator-value');
|
||||
expect(StatusAnnouncerElement.tagName).toBe('media-status-announcer');
|
||||
expect(VolumeIndicatorElement.tagName).toBe('media-volume-indicator');
|
||||
expect(VolumeIndicatorFillElement.tagName).toBe('media-volume-indicator-fill');
|
||||
expect(VolumeIndicatorValueElement.tagName).toBe('media-volume-indicator-value');
|
||||
expect(SeekIndicatorElement.tagName).toBe('media-seek-indicator');
|
||||
expect(SeekIndicatorValueElement.tagName).toBe('media-seek-indicator-value');
|
||||
});
|
||||
|
||||
it('uses authored HTML indicators as the mounted visual surface', () => {
|
||||
const host = document.createElement('media-volume-indicator');
|
||||
host.hidden = true;
|
||||
host.innerHTML = `
|
||||
<media-volume-indicator-fill>
|
||||
<media-volume-indicator-value></media-volume-indicator-value>
|
||||
</media-volume-indicator-fill>
|
||||
`;
|
||||
document.body.append(host);
|
||||
|
||||
const indicator = new LiveIndicator<VolumeIndicatorCore.State>({
|
||||
host,
|
||||
dataAttrs: VolumeIndicatorDataAttrs,
|
||||
render: (element, state) => {
|
||||
element
|
||||
.querySelector<HTMLElement>('media-volume-indicator-fill')
|
||||
?.style.setProperty(VolumeIndicatorCSSVars.fill, state.fill ?? '');
|
||||
const value = element.querySelector('media-volume-indicator-value');
|
||||
if (value) value.textContent = getVolumeIndicatorDisplayValue(state);
|
||||
},
|
||||
});
|
||||
|
||||
const liveElement = indicator.render({
|
||||
open: true,
|
||||
generation: 1,
|
||||
level: 'high',
|
||||
value: '60%',
|
||||
fill: '60%',
|
||||
min: false,
|
||||
max: false,
|
||||
transitionStarting: true,
|
||||
transitionEnding: false,
|
||||
});
|
||||
|
||||
expect(liveElement).toBe(host);
|
||||
expect(host.hidden).toBe(false);
|
||||
expect(document.body.querySelectorAll('media-volume-indicator')).toHaveLength(1);
|
||||
expect(liveElement.getAttribute('data-level')).toBe('high');
|
||||
expect(liveElement.querySelector('media-volume-indicator-value')?.textContent).toBe('60%');
|
||||
expect(
|
||||
liveElement
|
||||
.querySelector<HTMLElement>('media-volume-indicator-fill')
|
||||
?.style.getPropertyValue(VolumeIndicatorCSSVars.fill)
|
||||
).toBe('60%');
|
||||
|
||||
indicator.remove();
|
||||
expect(host.hidden).toBe(true);
|
||||
expect(document.body.querySelectorAll('media-volume-indicator')).toHaveLength(1);
|
||||
expect(host.hasAttribute('data-open')).toBe(false);
|
||||
expect(host.hasAttribute('data-level')).toBe(false);
|
||||
});
|
||||
|
||||
it('shares a visibility coordinator per container', () => {
|
||||
const container = document.createElement('div');
|
||||
const first = { close: vi.fn() };
|
||||
const second = { close: vi.fn() };
|
||||
|
||||
const coordinator = getIndicatorVisibilityCoordinator(container);
|
||||
coordinator.register(first);
|
||||
coordinator.register(second);
|
||||
coordinator.show(second);
|
||||
|
||||
expect(getIndicatorVisibilityCoordinator(container)).toBe(coordinator);
|
||||
expect(first.close).toHaveBeenCalledOnce();
|
||||
expect(second.close).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { getSeekIndicatorDisplayValue, SeekIndicatorCore, SeekIndicatorDataAttrs } from '@videojs/core';
|
||||
import { createTransition } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap } from '@videojs/element';
|
||||
|
||||
import { InputIndicatorElement } from '../input-indicators/input-indicator-element';
|
||||
import { LiveIndicator } from '../input-indicators/live-indicator';
|
||||
|
||||
export class SeekIndicatorElement extends InputIndicatorElement<SeekIndicatorCore.State> {
|
||||
static readonly tagName = 'media-seek-indicator';
|
||||
|
||||
static override properties = {
|
||||
closeDelay: { type: Number, attribute: 'close-delay' },
|
||||
} satisfies PropertyDeclarationMap<'closeDelay'>;
|
||||
|
||||
closeDelay: number | undefined;
|
||||
|
||||
readonly #core = new SeekIndicatorCore();
|
||||
readonly #transition = createTransition();
|
||||
readonly #liveIndicator = new LiveIndicator({
|
||||
host: this,
|
||||
dataAttrs: SeekIndicatorDataAttrs,
|
||||
render: renderSeekIndicator,
|
||||
});
|
||||
|
||||
protected get core() {
|
||||
return this.#core;
|
||||
}
|
||||
|
||||
protected get transition() {
|
||||
return this.#transition;
|
||||
}
|
||||
|
||||
protected get liveIndicator() {
|
||||
return this.#liveIndicator;
|
||||
}
|
||||
|
||||
protected override syncCoreProps(): void {
|
||||
this.#core.setProps({ closeDelay: this.closeDelay });
|
||||
}
|
||||
}
|
||||
|
||||
function renderSeekIndicator(element: HTMLElement, state: SeekIndicatorCore.State): void {
|
||||
const value = element.querySelector('media-seek-indicator-value');
|
||||
if (!value) return;
|
||||
|
||||
value.textContent = getSeekIndicatorDisplayValue(state);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { MediaElement } from '../media-element';
|
||||
|
||||
export class SeekIndicatorValueElement extends MediaElement {
|
||||
static readonly tagName = 'media-seek-indicator-value';
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { StatusAnnouncerCore } from '@videojs/core';
|
||||
import { getMediaSnapshot, subscribeToInputActions } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
|
||||
import { ContextConsumer } from '@videojs/element/context';
|
||||
|
||||
import { containerContext, playerContext } from '../../player/context';
|
||||
import { PlayerController } from '../../player/player-controller';
|
||||
import { MediaElement } from '../media-element';
|
||||
|
||||
export class StatusAnnouncerElement extends MediaElement {
|
||||
static readonly tagName = 'media-status-announcer';
|
||||
|
||||
static override properties = {
|
||||
closeDelay: { type: Number, attribute: 'close-delay' },
|
||||
} satisfies PropertyDeclarationMap<'closeDelay'>;
|
||||
|
||||
closeDelay: number | undefined;
|
||||
|
||||
readonly #core = new StatusAnnouncerCore();
|
||||
readonly #player = new PlayerController(this, playerContext);
|
||||
readonly #container = new ContextConsumer(this, {
|
||||
context: containerContext,
|
||||
callback: () => this.#reconnect(),
|
||||
subscribe: true,
|
||||
});
|
||||
|
||||
#disconnect: AbortController | null = null;
|
||||
#inputActionUnsubscribe: (() => void) | null = null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.destroyed) return;
|
||||
|
||||
this.setAttribute('role', 'status');
|
||||
|
||||
this.#disconnect = new AbortController();
|
||||
this.#core.state.subscribe(() => this.requestUpdate(), { signal: this.#disconnect.signal });
|
||||
this.#reconnect();
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.#inputActionUnsubscribe?.();
|
||||
this.#inputActionUnsubscribe = null;
|
||||
this.#disconnect?.abort();
|
||||
this.#disconnect = null;
|
||||
}
|
||||
|
||||
override destroyCallback(): void {
|
||||
this.#inputActionUnsubscribe?.();
|
||||
this.#core.destroy();
|
||||
super.destroyCallback();
|
||||
}
|
||||
|
||||
protected override willUpdate(changed: PropertyValues): void {
|
||||
super.willUpdate(changed);
|
||||
this.#core.setProps({ closeDelay: this.closeDelay });
|
||||
}
|
||||
|
||||
protected override update(changed: PropertyValues): void {
|
||||
super.update(changed);
|
||||
|
||||
const label = this.#core.state.current.label;
|
||||
if (label) {
|
||||
this.setAttribute('aria-label', label);
|
||||
} else {
|
||||
this.removeAttribute('aria-label');
|
||||
}
|
||||
}
|
||||
|
||||
#reconnect(): void {
|
||||
this.#inputActionUnsubscribe?.();
|
||||
this.#inputActionUnsubscribe = null;
|
||||
|
||||
const container = this.#container.value?.container;
|
||||
if (!container) return;
|
||||
|
||||
this.#inputActionUnsubscribe = subscribeToInputActions(container, (event) => {
|
||||
this.#core.processEvent(event, getMediaSnapshot(this.#player.value));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
getStatusIndicatorDisplayValue,
|
||||
type InputAction,
|
||||
StatusIndicatorCore,
|
||||
StatusIndicatorDataAttrs,
|
||||
} from '@videojs/core';
|
||||
import { createTransition } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap } from '@videojs/element';
|
||||
|
||||
import { InputIndicatorElement } from '../input-indicators/input-indicator-element';
|
||||
import { LiveIndicator } from '../input-indicators/live-indicator';
|
||||
|
||||
export class StatusIndicatorElement extends InputIndicatorElement<StatusIndicatorCore.State> {
|
||||
static readonly tagName = 'media-status-indicator';
|
||||
|
||||
static override properties = {
|
||||
actions: { type: String },
|
||||
closeDelay: { type: Number, attribute: 'close-delay' },
|
||||
} satisfies PropertyDeclarationMap<'actions' | 'closeDelay'>;
|
||||
|
||||
actions: string | undefined;
|
||||
closeDelay: number | undefined;
|
||||
|
||||
readonly #core = new StatusIndicatorCore();
|
||||
readonly #transition = createTransition();
|
||||
readonly #liveIndicator = new LiveIndicator({
|
||||
host: this,
|
||||
dataAttrs: StatusIndicatorDataAttrs,
|
||||
render: renderStatusIndicator,
|
||||
});
|
||||
|
||||
protected get core() {
|
||||
return this.#core;
|
||||
}
|
||||
|
||||
protected get transition() {
|
||||
return this.#transition;
|
||||
}
|
||||
|
||||
protected get liveIndicator() {
|
||||
return this.#liveIndicator;
|
||||
}
|
||||
|
||||
protected override syncCoreProps(): void {
|
||||
this.#core.setProps({ actions: parseActions(this.actions), closeDelay: this.closeDelay });
|
||||
}
|
||||
}
|
||||
|
||||
function parseActions(actions: string | undefined): readonly InputAction[] | undefined {
|
||||
return actions?.split(/[\s,]+/).filter(Boolean) as readonly InputAction[] | undefined;
|
||||
}
|
||||
|
||||
function renderStatusIndicator(element: HTMLElement, state: StatusIndicatorCore.State): void {
|
||||
const value = element.querySelector('media-status-indicator-value');
|
||||
if (!value) return;
|
||||
|
||||
value.textContent = getStatusIndicatorDisplayValue(state);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { MediaElement } from '../media-element';
|
||||
|
||||
export class StatusIndicatorValueElement extends MediaElement {
|
||||
static readonly tagName = 'media-status-indicator-value';
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
getVolumeIndicatorDisplayValue,
|
||||
VolumeIndicatorCore,
|
||||
VolumeIndicatorCSSVars,
|
||||
VolumeIndicatorDataAttrs,
|
||||
} from '@videojs/core';
|
||||
import { createTransition } from '@videojs/core/dom';
|
||||
import type { PropertyDeclarationMap } from '@videojs/element';
|
||||
|
||||
import { InputIndicatorElement } from '../input-indicators/input-indicator-element';
|
||||
import { LiveIndicator } from '../input-indicators/live-indicator';
|
||||
|
||||
export class VolumeIndicatorElement extends InputIndicatorElement<VolumeIndicatorCore.State> {
|
||||
static readonly tagName = 'media-volume-indicator';
|
||||
|
||||
static override properties = {
|
||||
closeDelay: { type: Number, attribute: 'close-delay' },
|
||||
} satisfies PropertyDeclarationMap<'closeDelay'>;
|
||||
|
||||
closeDelay: number | undefined;
|
||||
|
||||
readonly #core = new VolumeIndicatorCore();
|
||||
readonly #transition = createTransition();
|
||||
readonly #liveIndicator = new LiveIndicator({
|
||||
host: this,
|
||||
dataAttrs: VolumeIndicatorDataAttrs,
|
||||
render: renderVolumeIndicator,
|
||||
});
|
||||
|
||||
protected get core() {
|
||||
return this.#core;
|
||||
}
|
||||
|
||||
protected get transition() {
|
||||
return this.#transition;
|
||||
}
|
||||
|
||||
protected get liveIndicator() {
|
||||
return this.#liveIndicator;
|
||||
}
|
||||
|
||||
protected override syncCoreProps(): void {
|
||||
this.#core.setProps({ closeDelay: this.closeDelay });
|
||||
}
|
||||
}
|
||||
|
||||
function renderVolumeIndicator(element: HTMLElement, state: VolumeIndicatorCore.State): void {
|
||||
const fill = element.querySelector<HTMLElement>('media-volume-indicator-fill');
|
||||
const value = element.querySelector('media-volume-indicator-value');
|
||||
|
||||
if (state.fill) {
|
||||
fill?.style.setProperty(VolumeIndicatorCSSVars.fill, state.fill);
|
||||
} else {
|
||||
fill?.style.removeProperty(VolumeIndicatorCSSVars.fill);
|
||||
}
|
||||
|
||||
if (value) {
|
||||
value.textContent = getVolumeIndicatorDisplayValue(state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { MediaElement } from '../media-element';
|
||||
|
||||
export class VolumeIndicatorFillElement extends MediaElement {
|
||||
static readonly tagName = 'media-volume-indicator-fill';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { MediaElement } from '../media-element';
|
||||
|
||||
export class VolumeIndicatorValueElement extends MediaElement {
|
||||
static readonly tagName = 'media-volume-indicator-value';
|
||||
}
|
||||
Reference in New Issue
Block a user