Skip to content

Commit f30acf1

Browse files
committed
fix(studio): keep hidden state on expanded sub-composition rows
An expanded sub-composition child row is built from a manifest clip, which carries none of the host element's attributes, so data-hidden never reached it. The eye on that row therefore always reported the element visible: the first click hid it, and every click after wrote data-hidden again instead of removing it. The element could not be shown again, not even after a reload, because the attribute was already in the source. The flat store element for the same child is built with its host element, so the child row inherits hidden, timelineLocked and timelineRole from it.
1 parent 57781d4 commit f30acf1

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

‎packages/studio/src/player/hooks/useExpandedTimelineElements.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,35 @@ describe("buildExpandedElements", () => {
221221
expect(child.key).toBe(expectedStoreKey);
222222
});
223223

224+
// Regression: a child row is built from a manifest clip, which carries none of
225+
// the host element's attributes. Reading hidden off the manifest left every
226+
// expanded row reporting itself visible, so the eye wrote data-hidden a second
227+
// time instead of removing it and the element could never be shown again.
228+
it("inherits hidden and locked state from the flat store element", () => {
229+
const elements = [
230+
el({ id: "s1", domId: "s1", start: 0, duration: 14 }),
231+
el({
232+
id: "eyebrow",
233+
key: "index.html#eyebrow",
234+
domId: "eyebrow",
235+
start: 0,
236+
duration: 14,
237+
hidden: true,
238+
timelineLocked: true,
239+
}),
240+
];
241+
const manifest = [
242+
clip({ id: "s1", start: 0, duration: 14 }),
243+
clip({ id: "eyebrow", start: 0, duration: 14 }),
244+
];
245+
const parentMap = new Map([["eyebrow", "s1"]]);
246+
247+
const out = buildExpandedElements(elements, manifest, parentMap, "s1", "s1");
248+
const child = out.find((e) => e.domId === "eyebrow")!;
249+
expect(child.hidden).toBe(true);
250+
expect(child.timelineLocked).toBe(true);
251+
});
252+
224253
// Sub-comp internals (group + pills) have no data-start, so they're not in the
225254
// manifest. They arrive as DOM children and must still expand under their host.
226255
it("expands DOM-only sub-comp children (no manifest clip) under the host", () => {

‎packages/studio/src/player/hooks/useExpandedTimelineElements.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ interface DisplayBounds {
134134
track: number;
135135
}
136136

137+
/**
138+
* State that lives on the live host element, not in the clip manifest:
139+
* `data-hidden`, `data-timeline-locked`, `data-timeline-role`. A child row is
140+
* built from a manifest clip with no hostEl to read, so
141+
* createTimelineElementFromManifestClip cannot see any of it. The flat store
142+
* element for the same child WAS built with one, so it is inherited from there.
143+
*
144+
* Without this the eye on an expanded child always reported the row visible, so
145+
* clicking it wrote data-hidden again instead of removing it, and a hidden child
146+
* could never be shown again (not even after a reload, since the attribute is in
147+
* the source).
148+
*/
149+
function hostElementState(flat: TimelineElement | undefined): Partial<TimelineElement> {
150+
if (!flat) return {};
151+
return {
152+
hidden: flat.hidden,
153+
timelineLocked: flat.timelineLocked,
154+
timelineRole: flat.timelineRole,
155+
};
156+
}
157+
137158
// `display` bounds come from the top-level scene clip (where the expanded row is
138159
// drawn). `editBasis` comes from the child's immediate sub-comp host: its absolute
139160
// start anchors local-time edits and its compositionSrc is the file edits write to.
@@ -143,6 +164,7 @@ function buildChildElements(
143164
display: DisplayBounds,
144165
editBasis: { start: number; sourceFile: string | undefined },
145166
expandedHostKey: string,
167+
elements: readonly TimelineElement[],
146168
): TimelineElement[] {
147169
const result: TimelineElement[] = [];
148170
for (const child of siblings) {
@@ -170,6 +192,7 @@ function buildChildElements(
170192
});
171193
result.push({
172194
...base,
195+
...hostElementState(elements.find((element) => element.key === key)),
173196
key,
174197
start: clamped.start,
175198
duration: clamped.duration,
@@ -281,6 +304,7 @@ export function buildExpandedElements(
281304
},
282305
editBasis,
283306
parentKey,
307+
elements,
284308
);
285309
if (expanded.length === 0) return filterToTopLevel(elements, parentMap);
286310

0 commit comments

Comments
 (0)