diff --git a/packages/player/src/slideshow/hyperframes-slideshow.test.ts b/packages/player/src/slideshow/hyperframes-slideshow.test.ts index f86b862e28..ebf8f8d26e 100644 --- a/packages/player/src/slideshow/hyperframes-slideshow.test.ts +++ b/packages/player/src/slideshow/hyperframes-slideshow.test.ts @@ -2179,3 +2179,76 @@ describe(" Fix 4 — back affordance (postMessage only; c el.remove(); }); }); + +// --------------------------------------------------------------------------- +// Mechanical `interactive` attribute on inner +// --------------------------------------------------------------------------- +// The slideshow auto-applies the `interactive` attribute to every inner +// , so clickable controls, links, native media controls, +// and custom players inside the composition iframe receive pointer events +// without the author having to remember the attribute. The player's default +// is `pointer-events: none` on the iframe; `interactive` flips it to `auto` +// via the `:host([interactive])` rule in player styles. +// --------------------------------------------------------------------------- +describe(" auto-sets `interactive` on inner ", () => { + beforeEach(async () => { + await import("./hyperframes-slideshow.js"); + }); + + const tick = () => new Promise((r) => setTimeout(r, 0)); + + it("inner gets `interactive` attribute after mount", async () => { + const el = document.createElement("hyperframes-slideshow"); + const player = document.createElement("hyperframes-player"); + el.appendChild(player); + document.body.appendChild(el); + + // Allow the deferred initTimer macrotask to run. + await tick(); + + expect(player.hasAttribute("interactive")).toBe(true); + expect(player.getAttribute("interactive")).toBe(""); + + el.remove(); + }); + + it("preserves any author-supplied `interactive` attribute value verbatim", async () => { + const el = document.createElement("hyperframes-slideshow"); + const player = document.createElement("hyperframes-player"); + // Preserve any author-supplied `interactive` value verbatim. Note: the + // CSS rule `:host([interactive])` is presence-based per HTML + // boolean-attribute convention, so the runtime behavior is identical + // regardless of the value — the attribute always enables pointer + // events. The preservation guarantee here is about DOM hygiene + // (idempotent mechanical wire-up, no clobber on re-runs), not a + // runtime opt-out — `interactive="false"` is NOT an opt-out. + player.setAttribute("interactive", "false"); + el.appendChild(player); + document.body.appendChild(el); + + await tick(); + + expect(player.getAttribute("interactive")).toBe("false"); + + el.remove(); + }); + + it("dynamically-inserted children also get `interactive`", async () => { + const el = document.createElement("hyperframes-slideshow"); + document.body.appendChild(el); + + await tick(); + + // Late insertion — picked up by the MutationObserver. + const player = document.createElement("hyperframes-player"); + el.appendChild(player); + + // MutationObserver callbacks deliver on a microtask; flush twice to be safe. + await tick(); + await tick(); + + expect(player.hasAttribute("interactive")).toBe(true); + + el.remove(); + }); +}); diff --git a/packages/player/src/slideshow/hyperframes-slideshow.ts b/packages/player/src/slideshow/hyperframes-slideshow.ts index 167a2d3213..46f8f60923 100644 --- a/packages/player/src/slideshow/hyperframes-slideshow.ts +++ b/packages/player/src/slideshow/hyperframes-slideshow.ts @@ -167,6 +167,7 @@ export class HyperframesSlideshow extends HTMLElement { private initGeneration = 0; private _muted = false; private mediaWireInterval: ReturnType | null = null; + private playerObserver: MutationObserver | null = null; private applyingRemoteMedia = false; private lastMediaTimeBroadcastMs = 0; private audienceMutedPlaybackKeys = new Set(); @@ -215,6 +216,7 @@ export class HyperframesSlideshow extends HTMLElement { window.addEventListener("message", this.onMessage); document.addEventListener("fullscreenchange", this.onFsChange); this.initChannel(); + this.observeInteractivePlayers(); // Defer player-dependent init to a macrotask so that child elements are // parsed before we query for . This matters when the // bundle is loaded synchronously (e.g.