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:
Vance Ingalls
2026-06-24 21:31:39 -07:00
committed by GitHub
parent 364992203e
commit 64eaad7d69
3 changed files with 115 additions and 3 deletions
@@ -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;