import type { ButtonState, InferComponentState, InferMediaState, MediaButtonComponent, StateAttrMap, } from '@videojs/core'; import { applyElementProps, applyStateDataAttrs, createButton, HOTKEY_SHORTCUT_CHANGE_EVENT, logMissingFeature, type UIEvent, } from '@videojs/core/dom'; import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element'; import type { State } from '@videojs/store'; import type { PlayerController } from '../player/player-controller'; import { AriaKeyShortcutsController } from './hotkey/aria-key-shortcuts-controller'; import { MediaElement } from './media-element'; /** Abstract base for HTML custom elements that render a media-control button. */ export abstract class MediaButtonElement 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>; protected abstract readonly mediaState: PlayerController | undefined>; protected abstract activate(state: InferMediaState, event?: UIEvent): void; protected getIsButtonDisabled(): boolean { return this.disabled || !this.mediaState.value; } protected handleActivate(event: UIEvent): void { this.activate(this.mediaState.value!, event); } /** Override to set the hotkey action name for `aria-keyshortcuts`. */ protected readonly hotkeyAction: string | undefined = undefined; /** Override to match hotkeys that use action values, such as seek steps. */ protected get hotkeyValue(): number | undefined { return undefined; } get $state(): State { return this.core.state; } #disconnect: AbortController | null = null; #hotkeyRegistry: AriaKeyShortcutsController | null = null; #lastHotkeyShortcut: string | undefined; override connectedCallback(): void { super.connectedCallback(); if (this.destroyed) return; if (this.hotkeyAction && !this.#hotkeyRegistry) { this.#hotkeyRegistry = new AriaKeyShortcutsController(this, this.hotkeyAction, { value: () => this.hotkeyValue, }); } this.#disconnect = new AbortController(); const buttonProps = createButton({ onActivate: (event) => this.handleActivate(event), isDisabled: () => this.getIsButtonDisabled(), }); 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; } /** Returns the button's current label derived from media state. */ getLabel(): string | undefined { return this.core.state.current.label || undefined; } getShortcut(): string | undefined { return this.#hotkeyRegistry?.shortcut; } 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; this.#syncHotkeyShortcut(); if (!media) return; this.core.setMedia(media); const state = this.core.getState(); applyElementProps(this, { ...this.core.getAttrs?.(state), 'aria-keyshortcuts': this.#hotkeyRegistry?.aria, }); applyStateDataAttrs(this, state, this.stateAttrMap); } #syncHotkeyShortcut(): void { const shortcut = this.getShortcut(); if (shortcut === this.#lastHotkeyShortcut) return; this.#lastHotkeyShortcut = shortcut; this.dispatchEvent(new CustomEvent(HOTKEY_SHORTCUT_CHANGE_EVENT)); } }