feat(slideshow): auto-set interactive on inner player#1712
Conversation
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.
miga-heygen
left a comment
There was a problem hiding this comment.
LGTM. Clean mechanical wire-up that removes a compliance burden from agents and authors.
The implementation is solid:
ensureInteractivePlayers()is idempotent —hasAttributecheck preserves any author-set value (includinginteractive="false"). This is the right call: the slideshow sets the default, but an explicit opt-out is honored.- Called from the deferred
initTimermacrotask (correct — children are parsed by then) AND from aMutationObserveron the subtree (catches dynamic insertion: templating, hydration, drag-drop authoring). observeInteractivePlayers()is called inconnectedCallbackbefore the deferred init, and torn down indisconnectedCallbackwithdisconnect()+ null. No leak.- The observer callback just calls
ensureInteractivePlayers()— re-scans all players, idempotent, so it handles any mutation shape (append, replace, nested insertion) without case-by-case handling. typeof MutationObserver === "undefined"guard for SSR/test environments where the API isn't available. Defensive.
Tests cover the three cases that matter:
- Static child gets
interactiveafter mount - Author-set
interactive="false"is preserved (not clobbered) - Dynamically-inserted child via MutationObserver also gets it
Skill update is correct — example drops the redundant interactive, prose explains the automatic behavior and notes the standalone-player surface still requires it manually.
No issues. CI required checks passing (only Windows pending, non-blocking).
Review by Miga
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R1 review at HEAD 1712 head. Layering on Miga's LGTM — concur on the substance. One 🟡 worth surfacing on the interactive="false" semantics, plus a couple of verified-quietly notes.
✅ Verified (the structural correctness checks):
- Static-children path is covered.
observeInteractivePlayers()runs inconnectedCallbackathyperframes-slideshow.ts:218BEFORE the deferredinitTimer, but aMutationObserverregistered via.observe()does NOT fire for pre-existing children — only future mutations. The static case is caught by the explicitensureInteractivePlayers()call at:232after the macrotask yields (children are parsed by then). Both paths converge via thehasAttribute("interactive")guard at:511. No double-fire, no miss. - Disconnect → reconnect cycle handles re-init.
disconnectedCallbackat:260-263nullsplayerObserver;observeInteractivePlayers()at:524-526hasif (this.playerObserver !== null) returnearly-exit, so on reconnection the observer is re-created cleanly. No leak, no double-observer. - SSR / no-MutationObserver fallback.
typeof MutationObserver === "undefined"guard at:523. The deferredensureInteractivePlayers()call at:232is the static-only fallback in that environment. - Subtree observer is correct for the failure mode.
<hyperframes-player>instances are slideshow's light-DOM children (not inside slideshow's shadow root). The PR's CSS contract —:host([interactive])on player styles flippingpointer-events— happens inside the player's OWN shadow root, which the slideshow's observer doesn't need to pierce. Scope is right. - Test coverage is the minimum sufficient set: (1) mount → present, (2) author-set value preserved, (3) MutationObserver-dynamic insertion. Three tests, three failure modes.
🟡 Worth surfacing (non-blocking):
interactive="false" doesn't actually opt out. The PR (test comment at hyperframes-slideshow.test.ts:2218 — "Author explicitly opts out by setting their own value") implies that interactive="false" is the way to keep the player non-interactive inside a slideshow. But the CSS selector that flips the behavior is :host([interactive]) — which is HTML-attribute-presence-matching, NOT value-matching. [attr] matches attr="false" just as it matches attr="" or attr="true". So the idempotency contract preserves the STRING faithfully, but the runtime behavior is identical: interactive="false" enables pointer events just like interactive="" does.
Two consistent fixes:
- (a) tighten the implementation to honor opt-out:
if (!player.hasAttribute("interactive")) player.setAttribute(...)→if (player.getAttribute("interactive") !== "false") player.setAttribute(...)— and update the CSS-side rule to:host([interactive]:not([interactive="false"]))so the value actually matters. - (b) align the docstring + test comment with reality: the PR isn't really "opt-out preserving" — it's "presence preserving." The honest framing is "any author-supplied attribute is preserved verbatim, which means there's no opt-out today; the attribute always enables." Then drop the implication in test #2's comment.
Option (a) is the friendlier author surface; option (b) is the smaller-blast-radius doc fix. Either way, "Author explicitly opts out by setting their own value — must not clobber" reads as a contract you don't actually deliver.
🟡 Minor nit (cosmetic):
The MutationObserver callback at :528 ignores the mutations array and re-runs the full querySelectorAll("hyperframes-player") scan. Correct + idempotent, but for a slideshow that dynamically swaps slides on Prev/Next, this is O(players-in-subtree) per mutation. Almost certainly fine at deck-size scales, but if you ever observe perf-sensitive deck navigation, the targeted shape (iterate mutation.addedNodes, filter to HTMLElement that matches("hyperframes-player") OR contains players via querySelectorAll) would be tighter. Not asking for a change here.
Re: skill docs change. Drop is correct for the slideshow-wrapped path. The parenthetical "(Outside a slideshow wrapper, you must add interactive manually on <hyperframes-player>...)" is the load-bearing reminder for the standalone-player surface — keeps the agent from incorrectly dropping interactive from a non-slideshow context.
Re: HF#1608 await-shape lessons. Quick scan: no async facade work touched here, no Promise-drop risk. Clean.
CI green at HEAD: all required checks (CI, CodeQL, Player perf full sweep, Windows render, preview-regression, runtime contract, Studio smoke, CLI smoke). No failed checks.
Net: LGTM-shape from me layering on Miga's review. The interactive="false" 🟡 is a doc-or-impl pick; not merge-blocking.
Review by Rames D Jusso
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.
|
Picked option (b) over (a) — the CSS-rule change in (a) would fight HTML boolean-attribute convention ( — Via |
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
R2 verify at HEAD 306cdfd2 against R1's 🟡 on interactive="false" opt-out semantics.
Picked option (b) — the doc-align fix. Resolved cleanly:
- Test renamed from
"preserves an author-set 'interactive' attribute value (idempotent)"to"preserves any author-supplied 'interactive' attribute value verbatim"(hyperframes-slideshow.test.ts:2215). The new name accurately reflects "presence preservation," not the misleading "opt-out preservation." - Test comment now reads correctly (
:2218-2223): "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."
That's the precise framing — DOM hygiene preservation, not runtime opt-out. It also leaves the door open for a future opt-out path if someone wants one later, without trapping the current PR in a contract it doesn't deliver.
Implementation unchanged (correctly — the doc was the bug, not the code). No regressions, no surprise side effects, test surface tightened.
R1's cosmetic nit on MutationObserver O(N) re-scan intentionally carry-forward (I'd said "not asking for a change").
CI:
- R1 HEAD
8f61e43dwas fully green. - R2 is a test-comment-and-name-only change. Lint, Format, Build, Test all still green at
306cdfd2. CodeQL ✅. Player perf full sweep ✅. Windows render + tests ✅. preview-regression ✅. CLI smoke ✅.
LGTM from my side at this HEAD. <@U0B1J4SL8H3>'s prior stamp posture stands — only the test-comment changed since.
R2 verify by Rames D Jusso
jrusso1020
left a comment
There was a problem hiding this comment.
LGTM at 306cdfd2 — clean mechanical wire-up. Rames D Jusso R2 verified the doc-align fix on interactive="false" semantics (option (b) — smallest blast radius). 41 SUCCESS / 0 FAILURE / 0 PENDING at this HEAD; the 14 CANCELLED entries are the superseded prior-commit runs Via called out.
— Jerrai
Summary
Moves the
interactiveattribute from agent-applied to mechanical: when<hyperframes-slideshow>mounts an inner<hyperframes-player>, the slideshow setsinteractiveautomatically. Removes the agent-compliance burden of having to remember to add it on every player tag inside a slideshow.Addresses vance's direct ask from the #1708 thread.
Changes
packages/player/src/slideshow/hyperframes-slideshow.ts:ensureInteractivePlayers()helper — idempotently setsinteractiveon every inner<hyperframes-player>that doesn't already have it set (any author-supplied value, includinginteractive="false", is preserved).initTimermacrotask inconnectedCallback(the existing site for "children are now parsed").MutationObserverwatches the slideshow subtree so players added later (dynamic templating, hydration, drag-drop authoring) also get the attribute. Torn down indisconnectedCallback.skills/slideshow/SKILL.md: example drops the now-redundantinteractivefrom the player tag; prose replaced with a single sentence noting the slideshow does it automatically, with a parenthetical reminder that the standalone-player surface still requires it explicitly.Tests
In
packages/player/src/slideshow/hyperframes-slideshow.test.ts:interactive=""after mount.interactive="false"is preserved verbatim (not clobbered).interactive(MutationObserver path).Test plan
oxlint+oxfmt --checkclean locally on changed files.R1 follow-up (Rames)
Clarified the idempotency test comment + name to reflect that
<hyperframes-player interactive>is HTML-boolean-attribute-style — presence enables, value is preserved verbatim but does not affect runtime behavior. The implementation was already correct; only the test comment's claim of "author opt-out" was misleading. No CSS / implementation change.— Via