Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/core/src/runtime/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,52 @@ describe("initSandboxRuntimeModular", () => {
expect(child.style.visibility).toBe("hidden");
});

it("keeps external composition hosts visible through their authored duration", async () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);

const child = document.createElement("div");
child.setAttribute("data-composition-id", "sub");
child.setAttribute("data-composition-src", "compositions/sub.html");
child.setAttribute("data-start", "0");
child.setAttribute("data-duration", "3");
root.appendChild(child);

const template = document.createElement("template");
template.id = "sub-template";
template.innerHTML = `
<div data-composition-id="sub" data-width="1920" data-height="1080">
<div id="hold-marker">HOLD ME</div>
</div>
`;
document.body.appendChild(template);

(window as Window & { __timelines?: Record<string, RuntimeTimelineLike> }).__timelines = {
main: createMockTimeline(3),
sub: createMockTimeline(1),
};

initSandboxRuntimeModular();
await new Promise<void>((resolve) => window.setTimeout(resolve, 0));

const player = (
window as Window & {
__player?: { renderSeek: (timeSeconds: number) => void };
}
).__player;
Comment on lines +198 to +205
expect(player).toBeDefined();
expect(child.querySelector("#hold-marker")?.textContent).toBe("HOLD ME");

player?.renderSeek(2);

expect(child.style.visibility).toBe("visible");
});

it("pads the root timeline to the authored composition schedule before seeking visibility", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,9 @@ export function initSandboxRuntimeModular(): void {
if (authoredEnd != null && !node.hasAttribute(AUTHORED_END_ATTR)) {
node.setAttribute(AUTHORED_END_ATTR, authoredEnd);
}
// Non-root compositions derive visible duration from timeline.
// Strip both data-duration AND data-end so the visibility system
// falls back to the GSAP timeline duration (parity with preview).
// Strip public timing attrs on non-root compositions after preserving
// authored values privately. Runtime timing can still distinguish
// authored host windows from live child timeline durations.
node.removeAttribute("data-duration");
node.removeAttribute("data-end");
}
Expand Down Expand Up @@ -1335,9 +1335,18 @@ export function initSandboxRuntimeModular(): void {
}
}

// Composition hosts must respect both the authored parent clip window
// and the child composition's own live timeline duration.
if (duration != null && duration > 0 && liveDuration != null) {
const usesExternalCompositionSlot = rawNode.hasAttribute("data-composition-src");

// Generic child compositions retain legacy behavior and respect both
// the authored parent clip window and the live child timeline duration.
// External composition hosts render into an authored slot, so a shorter
// child timeline should hold its final state through that slot.
if (
duration != null &&
duration > 0 &&
liveDuration != null &&
!usesExternalCompositionSlot
) {
Comment on lines +1338 to +1349
duration = Math.min(duration, liveDuration);
} else if ((duration == null || duration <= 0) && liveDuration != null) {
duration = liveDuration;
Expand Down
Loading