diff --git a/packages/sdk/src/engine/model.ts b/packages/sdk/src/engine/model.ts index 2efae69eb..6c61b9e41 100644 --- a/packages/sdk/src/engine/model.ts +++ b/packages/sdk/src/engine/model.ts @@ -37,7 +37,7 @@ export function findById(document: Document, id: string): Element | null { return resolveScoped(document, id); } -function escapeHfId(id: string): string { +export function escapeHfId(id: string): string { return id.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } @@ -76,8 +76,15 @@ export function resolveScoped(document: Document, id: string): Element | null { if (parts.length === 1) { const escaped = escapeHfId(id); const matches = Array.from(document.querySelectorAll(`[data-hf-id="${escaped}"]`)); - if (matches.length === 0) return null; - return matches.find((el) => isCanonicalScope(el)) ?? matches[0] ?? null; + if (matches.length > 0) { + return matches.find((el) => isCanonicalScope(el)) ?? matches[0] ?? null; + } + // Fall back to a sub-composition ROOT addressed by its composition id. A + // host element carries data-hf-id (its own leaf id) AND data-composition-id + // (the id studio passes when targeting the sub-comp root). data-hf-id takes + // precedence above; only when no hf-id matches do we treat the bare id as a + // composition id, making comp-ids first-class resolvable addresses. + return document.querySelector(`[data-composition-id="${escaped}"]`); } let context: Element | Document = document; diff --git a/packages/sdk/src/engine/mutate.gsap.test.ts b/packages/sdk/src/engine/mutate.gsap.test.ts index c752c8cbd..771eff236 100644 --- a/packages/sdk/src/engine/mutate.gsap.test.ts +++ b/packages/sdk/src/engine/mutate.gsap.test.ts @@ -32,6 +32,17 @@ function fresh(script = GSAP_SCRIPT) { return parseMutable(makeHtml(script)); } +// A sub-composition host: data-hf-id="hf-host" (its own leaf id) AND +// data-composition-id="sub-1" (the id studio passes when targeting the root). +function freshSubComp(script = GSAP_SCRIPT) { + return parseMutable( + `
+
+ +
`.trim(), + ); +} + function getScript(parsed: ReturnType): string { const doc = serializeDocument(parsed); const m = / + + `); + const comp = await openComposition(html); + // Target the sub-comp ROOT by its composition id. + const animId = comp.addGsapTween("sub-1", { + method: "to", + duration: 0.3, + properties: { x: 200 }, + }); + // The tween is filed under the host's own data-hf-id (canonical form), so + // it surfaces on the host element snapshot. + const host = comp.getElement("hf-host"); + expect(host?.animationIds).toContain(animId); + }); +}); + // ─── 4. Override-set keys for scoped ids ────────────────────────────────────── describe("override-set — scoped id keys", () => {