From 2105010c7f1f525ab89cc30506219a5dd49a64a7 Mon Sep 17 00:00:00 2001 From: Wesley Luyten Date: Wed, 11 Mar 2026 13:36:24 -0500 Subject: [PATCH] fix: attaching media like elements and upgrade (#889) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- package.json | 2 +- .../dom/media/custom-media-element/index.ts | 3 -- packages/html/src/store/container-mixin.ts | 41 +++++++++++++------ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index e97f77e8..9d86e550 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/core/src/dom/media/custom-media-element/index.ts b/packages/core/src/dom/media/custom-media-element/index.ts index 0f6e0a53..99194d69 100644 --- a/packages/core/src/dom/media/custom-media-element/index.ts +++ b/packages/core/src/dom/media/custom-media-element/index.ts @@ -434,9 +434,6 @@ export function CustomMediaMixin>( 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', ''); } }; } diff --git a/packages/html/src/store/container-mixin.ts b/packages/html/src/store/container-mixin.ts index 6210c205..d5e6ce27 100644 --- a/packages/html/src/store/container-mixin.ts +++ b/packages/html/src/store/container-mixin.ts @@ -46,14 +46,14 @@ export function createContainerMixin(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(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(context: PlayerC if (!store) return; const media = - this.querySelector('video, audio, [data-media-element]') ?? this.#getSlottedMedia(); + this.querySelector('video, audio') ?? this.#findMediaElement() ?? this.#getSlottedMedia(); if (!media) { this.#detach(); @@ -101,6 +107,10 @@ export function createContainerMixin(context: PlayerC return; } + if (isCustomMediaElement(media)) { + globalThis.customElements?.upgrade?.(media); + } + const target: PlayerTarget = { media, container: this, @@ -120,22 +130,27 @@ export function createContainerMixin(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;