diff --git a/src/js/media-controller.js b/src/js/media-controller.js
index 6951238c4..8073cf6bb 100644
--- a/src/js/media-controller.js
+++ b/src/js/media-controller.js
@@ -279,6 +279,13 @@ class MediaController extends MediaContainer {
unregisterMediaStateReceiver,
);
+ // Add all media request event listeners to the Associated Element. This allows any DOM element that
+ // is a descendant of any Associated Element (including the itself) to make requests
+ // for media state changes rather than constraining that exclusively to a Media State Receivers.
+ Object.keys(MediaUIEvents).forEach((key) => {
+ element.addEventListener(MediaUIEvents[key], this[`_handle${constToCamel(key, true)}`]);
+ });
+
associatedElementSubscriptions.set(element, unsubscribe);
}
@@ -289,6 +296,11 @@ class MediaController extends MediaContainer {
const unsubscribe = associatedElementSubscriptions.get(element);
unsubscribe();
associatedElementSubscriptions.delete(element);
+
+ // Remove all media UI event listeners
+ Object.keys(MediaUIEvents).forEach((key) => {
+ element.removeEventListener(MediaUIEvents[key], this[`_handle${constToCamel(key, true)}`]);
+ });
}
registerMediaStateReceiver(el) {
@@ -299,16 +311,6 @@ class MediaController extends MediaContainer {
els.push(el);
- // TODO: Update to handle all request events
- // Could just attach all releveant listeners to every associated el
- // or could use the `on${eventName}` prop detection method to know
- // which events the el intends to dispatch
- // The latter requires authors to actually follow that paradigm
- // which is probably a stretch
- Object.keys(MediaUIEvents).forEach((key) => {
- el.addEventListener(MediaUIEvents[key], this[`_handle${constToCamel(key, true)}`]);
- });
-
// TODO: Update to propagate all states when registered
if (this.media) {
propagateMediaState([el], MediaUIAttributes.MEDIA_PAUSED, this.media.paused);
@@ -331,10 +333,6 @@ class MediaController extends MediaContainer {
if (index < 0) return;
els.splice(index, 1);
- // Remove all media UI event listeners
- Object.keys(MediaUIEvents).forEach((key) => {
- el.removeEventListener(MediaUIEvents[key], this[`_handle${constToCamel(key, true)}`]);
- });
}
// Mimick the media element API, but use it to dispatch media UI events
@@ -432,11 +430,6 @@ const setAttr = (child, attrName, attrValue) => {
const isMediaSlotElementDescendant = (el) => !!el.closest?.('*[slot="media"]');
-const isUndefinedCustomElement = (el) => {
- const name = el?.nodeName.toLowerCase();
- return name.includes('-') && !window.customElements.get(name);
-};
-
/**
*
* @description This function will recursively check for any descendants (including the rootNode)
@@ -452,41 +445,44 @@ const traverseForMediaStateReceivers = (rootNode, mediaStateReceiverCallback) =>
if (isMediaSlotElementDescendant(rootNode)) {
return;
}
- // If the rootNode is a custom element that's not yet defined/ready, we don't yet know for sure
- // whether or not it or its descendants are Media State Receivers, so wait until it's
- // defined before attempting traversal.
- if (isUndefinedCustomElement(rootNode)) {
- const name = rootNode?.nodeName.toLowerCase();
+
+ const traverseForMediaStateReceiversSync = (rootNode, mediaStateReceiverCallback) => {
+ // The rootNode is itself a Media State Receiver
+ if (isMediaStateReceiver(rootNode)) {
+ mediaStateReceiverCallback(rootNode);
+ }
+
+ const { children = [] } = rootNode ?? {};
+ const shadowChildren = rootNode?.shadowRoot?.children ?? [];
+ const allChildren = [...children, ...shadowChildren];
+
+ // Traverse all children (including shadowRoot children) to see if they are/have Media State Receivers
+ allChildren.forEach(child => traverseForMediaStateReceivers(child, mediaStateReceiverCallback));
+ };
+
+ // Custom Elements (and *only* Custom Elements) must have a hyphen ("-") in their name. So, if the rootNode is
+ // a custom element (aka has a hyphen in its name), wait until it's defined before attempting traversal to determine
+ // whether or not it or its descendants are Media State Receivers.
+ // IMPORTANT NOTE: We're intentionally *always* waiting for the `whenDefined()` Promise to resolve here
+ // (instead of using `window.customElements.get(name)` to check if a custom element is already defined/registered)
+ // because we encountered some reliability issues with the custom element instances not being fully "ready", even if/when
+ // they are available in the registry via `window.customElements.get(name)`.
+ const name = rootNode?.nodeName.toLowerCase();
+ if (name.includes('-') && !isMediaStateReceiver(rootNode)) {
window.customElements.whenDefined(name).then(() => {
// Try/traverse again once the custom element is defined
- traverseForMediaStateReceivers(rootNode, mediaStateReceiverCallback);
+ traverseForMediaStateReceiversSync(rootNode, mediaStateReceiverCallback);
});
return;
};
- // The rootNode is itself a Media State Receiver
- if (isMediaStateReceiver(rootNode)) {
- mediaStateReceiverCallback(rootNode);
- }
-
- const { children = [] } = rootNode ?? {};
- const shadowChildren = rootNode?.shadowRoot?.children ?? [];
- const allChildren = [...children, ...shadowChildren];
-
- // Traverse all children (including shadowRoot children) to see if they are/have Media State Receivers
- allChildren.forEach(child => traverseForMediaStateReceivers(child, mediaStateReceiverCallback));
+ traverseForMediaStateReceiversSync(rootNode, mediaStateReceiverCallback);
};
const propagateMediaState = (els, stateName, val) => {
els.forEach(el => {
- /** @TODO confirm this is still needed; otherwise, remove (CJP) */
- // Don't propagate into media elements, UI can't live in