fix: attaching media like elements and upgrade (#889)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Wesley Luyten
2026-03-11 13:36:24 -05:00
committed by GitHub
co-authored by Copilot
parent 16eceb7f47
commit 2105010c7f
3 changed files with 29 additions and 17 deletions
+1 -1
View File
@@ -26,7 +26,7 @@
"dev:site": "turbo run dev --filter=site",
"dev": "turbo run dev --parallel",
"dev:packages": "turbo run dev --parallel --filter='./packages/*'",
"dev:sandbox": "turbo run dev --filter=@videojs/sandbox",
"dev:sandbox": "turbo watch dev --filter=@videojs/sandbox",
"lint": "biome check .",
"lint:fix": "biome check . --write",
"lint:fix:file": "biome check --write",
@@ -434,9 +434,6 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
connectedCallback(): void {
this.#init();
// Set after #init() so the shadow root exists; safe here since
// connectedCallback is spec-compliant for setAttribute (unlike constructor).
this.setAttribute('data-media-element', '');
}
};
}
+28 -13
View File
@@ -46,14 +46,14 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
super.connectedCallback();
this.#observer = new MutationObserver((records) => {
if (records.some(hasMediaNode)) this.#attachMedia();
if (records.some(hasMediaElement)) this.#attachMedia();
});
this.#observer.observe(this, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['data-media-element'],
attributeFilter: ['name'],
});
// Slotted media elements don't appear in the container's subtree,
@@ -80,12 +80,18 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
if (!slot) return null;
for (const el of slot.assignedElements({ flatten: true })) {
if (el instanceof HTMLMediaElement) return el;
if (isMediaElement(el)) return el as HTMLMediaElement;
}
return null;
}
#findMediaElement(): HTMLMediaElement | null {
const media = Array.from(this.children).find(isMediaElement);
if (media) return media as HTMLMediaElement;
return null;
}
#attachMedia() {
// Prefer the cached context value; fall back to `this.store` which
// ProviderMixin overrides when both mixins are applied to one element.
@@ -93,7 +99,7 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
if (!store) return;
const media =
this.querySelector<HTMLMediaElement>('video, audio, [data-media-element]') ?? this.#getSlottedMedia();
this.querySelector<HTMLMediaElement>('video, audio') ?? this.#findMediaElement() ?? this.#getSlottedMedia();
if (!media) {
this.#detach();
@@ -101,6 +107,10 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
return;
}
if (isCustomMediaElement(media)) {
globalThis.customElements?.upgrade?.(media);
}
const target: PlayerTarget = {
media,
container: this,
@@ -120,22 +130,27 @@ export function createContainerMixin<Store extends PlayerStore>(context: PlayerC
};
}
function isMediaNode(node: Node): boolean {
return node instanceof HTMLMediaElement || (node instanceof Element && node.hasAttribute('data-media-element'));
function isMediaElement(node: Node): boolean {
return node instanceof HTMLMediaElement || isCustomMediaElement(node);
}
function hasMediaNode(record: MutationRecord): boolean {
// Attribute mutation: data-media-element was added to a descendant
if (record.type === 'attributes' && record.target instanceof Element) {
return record.target.hasAttribute('data-media-element');
}
function isCustomMediaElement(node: Node): boolean {
return node instanceof HTMLElement && (node.localName.endsWith('-audio') || node.localName.endsWith('-video'));
}
function isMediaSlotElement(node: Node): boolean {
return node instanceof HTMLSlotElement && node.name === 'media';
}
function hasMediaElement(record: MutationRecord): boolean {
if (isMediaSlotElement(record.target)) return true;
for (const node of record.addedNodes) {
if (isMediaNode(node)) return true;
if (isMediaElement(node) || isMediaSlotElement(node)) return true;
}
for (const node of record.removedNodes) {
if (isMediaNode(node)) return true;
if (isMediaElement(node) || isMediaSlotElement(node)) return true;
}
return false;