feat(html): refactor attach contexts to carry state and setter (#1024)

Co-authored-by: Wesley Luyten <luwes@users.noreply.github.com>
This commit is contained in:
rahim
2026-03-19 00:49:40 -07:00
committed by GitHub
co-authored by Wesley Luyten
parent eabd06550d
commit c07da27ead
13 changed files with 143 additions and 52 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
import { containerAttachContext, playerContext } from '../player/context';
import { containerContext, playerContext } from '../player/context';
import { createContainerMixin } from '../store/container-mixin';
import { MediaElement } from '../ui/media-element';
const ContainerMixin = createContainerMixin({ playerContext, containerAttachContext });
const ContainerMixin = createContainerMixin({ playerContext, containerContext });
export class MediaContainerElement extends ContainerMixin(MediaElement) {
static readonly tagName = 'media-container';
+21 -11
View File
@@ -5,7 +5,7 @@ import { type Context, createContext } from '@videojs/element/context';
// Player Context
// ----------------------------------------
export const PLAYER_CONTEXT_KEY = Symbol('@videojs/player');
export const PLAYER_CONTEXT_KEY = Symbol.for('@videojs/player');
export type PlayerContextValue<Store extends PlayerStore = AnyPlayerStore> = Store;
@@ -22,23 +22,33 @@ export type PlayerContext<Store extends PlayerStore = AnyPlayerStore> = Context<
export const playerContext = createContext<PlayerContextValue, typeof PLAYER_CONTEXT_KEY>(PLAYER_CONTEXT_KEY);
// ----------------------------------------
// Attach Contexts
// Media Context
// ----------------------------------------
export const MEDIA_ATTACH_KEY = Symbol('@videojs/media-attach');
export const MEDIA_CONTEXT_KEY = Symbol.for('@videojs/media');
export type MediaAttachValue = (media: Media | null) => void;
export interface MediaContextValue {
media: Media | null;
setMedia: (media: Media | null) => void;
}
export type MediaAttachContext = Context<typeof MEDIA_ATTACH_KEY, MediaAttachValue>;
export type MediaContext = Context<typeof MEDIA_CONTEXT_KEY, MediaContextValue>;
export const mediaAttachContext = createContext<MediaAttachValue, typeof MEDIA_ATTACH_KEY>(MEDIA_ATTACH_KEY);
export const mediaContext = createContext<MediaContextValue, typeof MEDIA_CONTEXT_KEY>(MEDIA_CONTEXT_KEY);
export const CONTAINER_ATTACH_KEY = Symbol('@videojs/container-attach');
// ----------------------------------------
// Container Context
// ----------------------------------------
export type ContainerAttachValue = (container: MediaContainer | null) => void;
export const CONTAINER_CONTEXT_KEY = Symbol.for('@videojs/container');
export type ContainerAttachContext = Context<typeof CONTAINER_ATTACH_KEY, ContainerAttachValue>;
export interface ContainerContextValue {
container: MediaContainer | null;
setContainer: (container: MediaContainer | null) => void;
}
export const containerAttachContext = createContext<ContainerAttachValue, typeof CONTAINER_ATTACH_KEY>(
CONTAINER_ATTACH_KEY
export type ContainerContext = Context<typeof CONTAINER_CONTEXT_KEY, ContainerContextValue>;
export const containerContext = createContext<ContainerContextValue, typeof CONTAINER_CONTEXT_KEY>(
CONTAINER_CONTEXT_KEY
);
+4 -4
View File
@@ -11,7 +11,7 @@ import { combine, createStore } from '@videojs/store';
import { type ContainerMixin, createContainerMixin } from '../store/container-mixin';
import { createProviderMixin, type ProviderMixin } from '../store/provider-mixin';
import { containerAttachContext, mediaAttachContext, type PlayerContext, playerContext } from './context';
import { containerContext, mediaContext, type PlayerContext, playerContext } from './context';
import { PlayerController } from './player-controller';
export interface CreatePlayerConfig<Features extends AnyPlayerFeature[]> {
@@ -89,14 +89,14 @@ export function createPlayer(config: CreatePlayerConfig<AnyPlayerFeature[]>): Cr
const ProviderMixin = createProviderMixin<PlayerStore>({
playerContext,
mediaAttachContext,
containerAttachContext,
mediaContext,
containerContext,
factory: create,
});
const ContainerMixin = createContainerMixin<PlayerStore>({
playerContext,
containerAttachContext,
containerContext,
});
return {
+6 -6
View File
@@ -1,12 +1,12 @@
import type { MediaContainer, PlayerStore } from '@videojs/core/dom';
import { ContextConsumer } from '@videojs/element/context';
import type { MediaElementConstructor } from '@/ui/media-element';
import type { ContainerAttachContext, PlayerContext } from '../player/context';
import type { ContainerContext, PlayerContext } from '../player/context';
import type { PlayerConsumer, PlayerConsumerConstructor } from './types';
export interface ContainerMixinConfig<Store extends PlayerStore> {
playerContext: PlayerContext<Store>;
containerAttachContext: ContainerAttachContext;
containerContext: ContainerContext;
}
export type ContainerMixin<Store extends PlayerStore> = <Class extends MediaElementConstructor>(
@@ -15,9 +15,9 @@ export type ContainerMixin<Store extends PlayerStore> = <Class extends MediaElem
/**
* Create a mixin that consumes player context and registers itself as the
* container element with the provider via `containerAttachContext`.
* container element with the provider via `containerContext`.
*
* @param config - Container configuration with player and attach contexts.
* @param config - Container configuration with player and container contexts.
*/
export function createContainerMixin<Store extends PlayerStore>(
config: ContainerMixinConfig<Store>
@@ -39,9 +39,9 @@ export function createContainerMixin<Store extends PlayerStore>(
});
new ContextConsumer(this, {
context: config.containerAttachContext,
context: config.containerContext,
callback: (value) => {
this.#setContainer = value ?? null;
this.#setContainer = value?.setContainer ?? null;
if (this.isConnected) this.#setContainer?.(this);
},
subscribe: true,
@@ -2,20 +2,20 @@ import type { Media } from '@videojs/core/dom';
import { ContextEvent } from '@videojs/element/context';
import type { CustomElement } from '@videojs/utils/dom';
import type { AnyConstructor, Constructor } from '@videojs/utils/types';
import { type MediaAttachContext, mediaAttachContext } from '../player/context';
import { type MediaContext, mediaContext } from '../player/context';
export type MediaAttachMixin = <Class extends AnyConstructor<HTMLElement>>(BaseClass: Class) => Class;
/**
* Create a mixin that consumes `mediaAttachContext` and registers the
* Create a mixin that consumes `mediaContext` and registers the
* element as the media with the provider.
*
* Uses the raw context-request protocol so it works with any
* `HTMLElement` subclass — no `ReactiveControllerHost` required.
*
* @param context - The media attach context to consume.
* @param context - The media context to consume.
*/
export function createMediaAttachMixin(context: MediaAttachContext): MediaAttachMixin {
export function createMediaAttachMixin(context: MediaContext): MediaAttachMixin {
return <Class extends AnyConstructor<HTMLElement>>(BaseClass: Class) => {
class MediaAttachElement extends (BaseClass as unknown as Constructor<CustomElement>) {
#setMedia: ((media: Media | null) => void) | null = null;
@@ -34,7 +34,7 @@ export function createMediaAttachMixin(context: MediaAttachContext): MediaAttach
this,
(value, unsubscribe) => {
if (unsubscribe) this.#unsubscribe = unsubscribe;
this.#setMedia = value ?? null;
this.#setMedia = value?.setMedia ?? null;
if (this.isConnected) {
this.#setMedia?.(this.getMediaTarget());
}
@@ -57,4 +57,4 @@ export function createMediaAttachMixin(context: MediaAttachContext): MediaAttach
};
}
export const MediaAttachMixin = createMediaAttachMixin(mediaAttachContext);
export const MediaAttachMixin = createMediaAttachMixin(mediaContext);
+16 -14
View File
@@ -2,13 +2,13 @@ import type { Media, MediaContainer, PlayerStore, PlayerTarget } from '@videojs/
import { ContextProvider } from '@videojs/element/context';
import { isNull } from '@videojs/utils/predicate';
import type { MediaElementConstructor } from '@/ui/media-element';
import type { ContainerAttachContext, MediaAttachContext, PlayerContext } from '../player/context';
import type { ContainerContext, MediaContext, PlayerContext } from '../player/context';
import type { PlayerProvider, PlayerProviderConstructor } from './types';
export interface ProviderMixinConfig<Store extends PlayerStore> {
playerContext: PlayerContext<Store>;
mediaAttachContext: MediaAttachContext;
containerAttachContext: ContainerAttachContext;
mediaContext: MediaContext;
containerContext: ContainerContext;
factory: () => Store;
}
@@ -20,9 +20,9 @@ export type ProviderMixin<Store extends PlayerStore> = <Class extends MediaEleme
* Create a mixin that provides player context to descendant elements and
* owns the `store.attach()` lifecycle.
*
* Media and container elements register themselves via attach contexts —
* setter callbacks flowing downward from the provider. When a media element
* is available, the provider calls `store.attach({ media, container })`.
* Media and container elements register themselves via media/container
* contexts that carry both the current value and a setter. When a media
* element is available, the provider calls `store.attach({ media, container })`.
*
* As a fallback for plain `<video>`/`<audio>` that can't consume context,
* the provider queries its subtree after a microtask.
@@ -43,12 +43,14 @@ export function createProviderMixin<Store extends PlayerStore>(
#setMedia = (media: Media | null): void => {
if (this.#media === media) return;
this.#media = media;
this.#mediaProvider.setValue({ media, setMedia: this.#setMedia });
this.#tryAttach();
};
#setContainer = (container: MediaContainer | null): void => {
if (this.#container === container) return;
this.#container = container;
this.#containerProvider.setValue({ container, setContainer: this.#setContainer });
this.#tryAttach();
};
@@ -57,14 +59,14 @@ export function createProviderMixin<Store extends PlayerStore>(
initialValue: this.store,
});
#mediaAttachProvider = new ContextProvider(this, {
context: config.mediaAttachContext,
initialValue: this.#setMedia,
#mediaProvider = new ContextProvider(this, {
context: config.mediaContext,
initialValue: { media: this.#media, setMedia: this.#setMedia },
});
#containerAttachProvider = new ContextProvider(this, {
context: config.containerAttachContext,
initialValue: this.#setContainer,
#containerProvider = new ContextProvider(this, {
context: config.containerContext,
initialValue: { container: this.#container, setContainer: this.#setContainer },
});
get store(): Store {
@@ -78,8 +80,8 @@ export function createProviderMixin<Store extends PlayerStore>(
override connectedCallback() {
super.connectedCallback();
this.#playerProvider.setValue(this.store);
this.#mediaAttachProvider.setValue(this.#setMedia);
this.#containerAttachProvider.setValue(this.#setContainer);
this.#mediaProvider.setValue({ media: this.#media, setMedia: this.#setMedia });
this.#containerProvider.setValue({ container: this.#container, setContainer: this.#setContainer });
this.#tryAttach();
this.#queueFallbackDiscovery();
}