fix(studio): expand sub-comp children against their resolved parent host

buildExpandedElements synthesized DOM-only sub-composition children against
the top-level element rather than the parentHost it resolves immediately
after. Under two-level nesting every child row therefore inherited the
top-level window instead of its own host's, so the rows drew at the wrong
offset and duration.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 18:04:37 +02:00
parent 3f0c20f633
commit 12546985f5
2 changed files with 53 additions and 5 deletions
@@ -243,6 +243,45 @@ describe("buildExpandedElements", () => {
// The host row is replaced by its children.
expect(out.some((e) => e.domId === "scene-host")).toBe(false);
});
// Regression: DOM-only children were synthesized against the TOP-LEVEL element
// instead of the sub-comp host they actually live in, so every child row read
// the whole top-level window rather than its host's.
it("spans DOM-only children over their nested host's window, not the top-level one", () => {
const elements = [
el({ id: "scene-host", start: 0, duration: 20, compositionSrc: "scene.html" }),
];
const manifest = [
clip({ id: "scene-host", start: 0, duration: 20, compositionSrc: "scene.html" }),
clip({ id: "sub-host", start: 5, duration: 6, compositionSrc: "sub.html" }),
];
const parentMap = new Map([
["sub-host", "scene-host"],
["pill-1", "sub-host"],
]);
const domClipChildren = [
{
id: "pill-1",
parentId: "sub-host",
hostId: "sub-host",
label: "pill-1",
stackingContextId: "css:0.0",
},
];
const out = buildExpandedElements(
elements,
manifest,
parentMap,
"scene-host",
"sub-host",
domClipChildren,
);
const pill = out.find((e) => e.domId === "pill-1")!;
expect(pill.start).toBe(5);
expect(pill.duration).toBe(6);
expect(pill.sourceFile).toBe("sub.html");
});
});
describe("resolveTimelineExpansionRawId", () => {
@@ -209,7 +209,13 @@ function buildChildElements(
function domSiblingClips(
domClipChildren: DomClipChild[],
siblingParentId: string,
host: TimelineElement,
host: {
id: string | null;
start: number;
duration: number;
track: number;
compositionSrc?: string | null;
},
): ClipManifestClip[] {
return domClipChildren
.filter((c) => c.parentId === siblingParentId)
@@ -243,20 +249,23 @@ export function buildExpandedElements(
const topLevelElement = elements.find((el) => el.id === topLevelId || el.domId === topLevelId);
if (!topLevelElement) return filterToTopLevel(elements, parentMap);
// The sub-comp host the children actually live in: top-level host for 1-level
// nesting, a nested host for deeper nesting. Its start/file anchor edits.
const parentHost = manifest.find((c) => c.id === siblingParentId);
// Prefer real manifest children; fall back to DOM-only sub-comp children
// (groups/pills) that have no data-start and thus never enter the manifest.
// Those are synthesized against the host they actually live in, not the
// top-level element, or every child row reads the whole top-level window.
const siblings = (() => {
const fromManifest = manifest.filter(
(c) => c.id != null && parentMap.get(c.id) === siblingParentId,
);
if (fromManifest.length > 0) return fromManifest;
return domSiblingClips(domClipChildren, siblingParentId, topLevelElement);
return domSiblingClips(domClipChildren, siblingParentId, parentHost ?? topLevelElement);
})();
if (siblings.length === 0) return filterToTopLevel(elements, parentMap);
// The sub-comp host the children actually live in: top-level host for 1-level
// nesting, a nested host for deeper nesting. Its start/file anchor edits.
const parentHost = manifest.find((c) => c.id === siblingParentId);
const editBasis = {
start: parentHost?.start ?? topLevelElement.start,
sourceFile: parentHost?.compositionSrc ?? topLevelElement.compositionSrc ?? undefined,