mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(slideshow): auto-set interactive on inner player (#1712)
* feat(slideshow): auto-set interactive on inner player The slideshow now sets the `interactive` attribute on its inner <hyperframes-player> instances at mount time, so pointer events reach the composition iframe automatically. Removes the agent-compliance burden of having to remember to add `interactive` on every player tag inside a slideshow. Idempotent: an author-supplied `interactive` attribute (any value, including `interactive="false"`) is preserved. A MutationObserver also picks up players inserted dynamically after the initial mount. Standalone player usage outside a slideshow still requires the explicit attribute — that surface is unchanged. Skill guidance at skills/slideshow/SKILL.md updated to reflect the automatic behavior. * docs(slideshow): clarify interactive attribute semantics Per Rames R1 review feedback: the test comment implied `interactive="false"` is an author opt-out, but `:host([interactive])` is presence-matching per HTML boolean-attribute convention — so any value (including "false") enables pointer events at runtime. The slideshow's mechanical wire-up preserves any author-supplied value verbatim for DOM hygiene, not as a runtime opt-out.
This commit is contained in:
@@ -2179,3 +2179,76 @@ describe("<hyperframes-slideshow> Fix 4 — back affordance (postMessage only; c
|
||||
el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mechanical `interactive` attribute on inner <hyperframes-player>
|
||||
// ---------------------------------------------------------------------------
|
||||
// The slideshow auto-applies the `interactive` attribute to every inner
|
||||
// <hyperframes-player>, 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("<hyperframes-slideshow> auto-sets `interactive` on inner <hyperframes-player>", () => {
|
||||
beforeEach(async () => {
|
||||
await import("./hyperframes-slideshow.js");
|
||||
});
|
||||
|
||||
const tick = () => new Promise<void>((r) => setTimeout(r, 0));
|
||||
|
||||
it("inner <hyperframes-player> 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 <hyperframes-player> 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,6 +167,7 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
private initGeneration = 0;
|
||||
private _muted = false;
|
||||
private mediaWireInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private playerObserver: MutationObserver | null = null;
|
||||
private applyingRemoteMedia = false;
|
||||
private lastMediaTimeBroadcastMs = 0;
|
||||
private audienceMutedPlaybackKeys = new Set<string>();
|
||||
@@ -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 <hyperframes-player>. This matters when the
|
||||
// bundle is loaded synchronously (e.g. <script src> in <head>), where
|
||||
@@ -225,7 +227,10 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
// setTimeout(0) macrotask yields to the parser so the children land first.
|
||||
this.initTimer = setTimeout(() => {
|
||||
this.initTimer = null;
|
||||
if (this.isConnected && !this.disconnected) void this.init();
|
||||
if (this.isConnected && !this.disconnected) {
|
||||
this.ensureInteractivePlayers();
|
||||
void this.init();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -252,6 +257,10 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
clearInterval(this.mediaWireInterval);
|
||||
this.mediaWireInterval = null;
|
||||
}
|
||||
if (this.playerObserver !== null) {
|
||||
this.playerObserver.disconnect();
|
||||
this.playerObserver = null;
|
||||
}
|
||||
this.audienceMediaUnlockButton?.remove();
|
||||
this.audienceMediaUnlockButton = null;
|
||||
this.audienceMutedPlaybackKeys.clear();
|
||||
@@ -490,6 +499,36 @@ export class HyperframesSlideshow extends HTMLElement {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner `<hyperframes-player>` instances inside a slideshow need the
|
||||
* `interactive` attribute so clickable controls, links, native media
|
||||
* controls, and custom players inside the composition iframe receive
|
||||
* pointer events (the player's default is `pointer-events: none`).
|
||||
*
|
||||
* Set it mechanically so authors / agents don't have to remember.
|
||||
* Idempotent: if the host already declared `interactive` (any value,
|
||||
* including `interactive="false"`), it is preserved.
|
||||
*/
|
||||
private ensureInteractivePlayers(): void {
|
||||
for (const player of this.querySelectorAll("hyperframes-player")) {
|
||||
if (!player.hasAttribute("interactive")) {
|
||||
player.setAttribute("interactive", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for `<hyperframes-player>` children added after the initial mount
|
||||
* (dynamic templating, hydration, drag-drop authoring) and apply the
|
||||
* `interactive` attribute to those too.
|
||||
*/
|
||||
private observeInteractivePlayers(): void {
|
||||
if (typeof MutationObserver === "undefined") return;
|
||||
if (this.playerObserver !== null) return;
|
||||
this.playerObserver = new MutationObserver(() => this.ensureInteractivePlayers());
|
||||
this.playerObserver.observe(this, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
private playerFrameDocument(player: Partial<PlayerElement> & HTMLElement): Document | null {
|
||||
const frame = player.iframeElement;
|
||||
if (!(frame instanceof HTMLIFrameElement)) return null;
|
||||
|
||||
Reference in New Issue
Block a user