Skip to content

Commit 537b987

Browse files
vanceingallsclaude
andcommitted
fix(studio): the sliver cull keeps the clip that holds a track's lanes
The header caret's aria-controls names the property lanes of the track's keyframe clip, and those lanes mount with that clip. When the clip was an interior sliver at fit zoom, nothing else kept it, so the cull dropped it and the caret named an element that was not there. It is now kept like a selected or focused clip: one more clip per track at most. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent 1376895 commit 537b987

2 files changed

Lines changed: 36 additions & 9 deletions

File tree

‎packages/studio/src/player/components/TimelineLanes.test.tsx‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,11 @@ interface RenderLanesOptions {
9191
snapGuide?: { time: number; type: "beat" | "clip-edge" | "playhead" } | null;
9292
withoutSelectHandler?: boolean;
9393
pps?: number;
94+
rowsVirtualized?: boolean;
9495
}
9596

9697
const lanePps = (options: RenderLanesOptions) => options.pps ?? 100;
98+
const lanesVirtualized = (options: RenderLanesOptions) => options.rowsVirtualized ?? false;
9799

98100
const selectHandlerFor = (
99101
options: RenderLanesOptions,
@@ -139,7 +141,7 @@ function renderLanes(options: RenderLanesOptions = {}): {
139141
rowHeights={rowHeights}
140142
rowGeometry={getTimelineRowGeometry(rowHeights)}
141143
virtualRows={displayTrackOrder.map((_, index) => ({ index, rowKey: index }))}
142-
rowsVirtualized={false}
144+
rowsVirtualized={lanesVirtualized(next)}
143145
focusedTargetId={null}
144146
logicalRows={buildTimelineLogicalRows({
145147
tracks,
@@ -334,6 +336,29 @@ describe("TimelineLanes disclosure target", () => {
334336
act(() => view.root.unmount());
335337
});
336338

339+
// At fit zoom a long track skips the clips hidden under its neighbours, and
340+
// the keyframed one among them holds the lanes the caret names.
341+
it("keeps the caret's target when the keyframed clip is a skipped sliver", () => {
342+
const clips = Array.from({ length: 20 }, (_, i) => ({
343+
...element(`c${i}`, TRACK_A),
344+
start: i * 0.5,
345+
duration: 0.5,
346+
}));
347+
const view = renderLanes({
348+
elements: clips,
349+
animations: new Map([["c2", [positionTween("c2")]]]),
350+
expandedClipIds: clips.map((clip) => clip.id),
351+
pps: 1,
352+
rowsVirtualized: true,
353+
});
354+
355+
expect(view.host.querySelector('[data-el-id="c1"]')).toBeNull();
356+
expect(view.host.querySelector('[data-el-id="c2"]')).not.toBeNull();
357+
expect(ariaControlsTarget(view.host)).not.toBeNull();
358+
expect(ariaControlsTargets(view.host).every(Boolean)).toBe(true);
359+
act(() => view.root.unmount());
360+
});
361+
337362
it("still resolves the caret's aria-controls while the layer is collapsed", () => {
338363
const view = renderLanes({ animations: ANIMATIONS, expandedClipIds: [] });
339364
const target = ariaControlsTarget(view.host);

‎packages/studio/src/player/components/TimelineLanes.tsx‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ export function TimelineLanes({
220220
if (!logicalRow) return null;
221221
const rowHeight = rowGeometry.getRowHeight(row);
222222
const els = tracks.find(([t]) => t === trackNum)?.[1] ?? [];
223+
// Only the selected/most-keyframed clip owns expanded lanes on a shared track.
224+
const keyframeClip = resolveTrackKeyframeClip(
225+
els,
226+
laneCounts,
227+
selectedElementId,
228+
selectedElementIds,
229+
);
230+
const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
223231
const renderElements = rowsVirtualized
224232
? withoutHiddenSlivers(
225233
queryTimelineClipIndex(
@@ -234,6 +242,8 @@ export function TimelineLanes({
234242
const key = getTimelineElementIdentity(el);
235243
return (
236244
pinnedClipIdentities.has(key) ||
245+
// It holds the lanes the header caret's aria-controls names.
246+
key === keyframeClipKey ||
237247
selectedElementId === key ||
238248
selectedElementIds.has(key) ||
239249
hoveredClip === key ||
@@ -269,14 +279,6 @@ export function TimelineLanes({
269279
});
270280
const isTrackHidden = els.length > 0 && els.every((element) => element.hidden === true);
271281
const isAudioTrack = els.length > 0 && els.some(isAudioTimelineElement);
272-
// Only the selected/most-keyframed clip owns expanded lanes on a shared track.
273-
const keyframeClip = resolveTrackKeyframeClip(
274-
els,
275-
laneCounts,
276-
selectedElementId,
277-
selectedElementIds,
278-
);
279-
const keyframeClipKey = keyframeClip?.key ?? keyframeClip?.id;
280282
const rowExpanded = isTrackRowExpanded(els, expandedClipIds);
281283
// How tall a clip BAR is drawn. An expanded row is mostly lanes, and a
282284
// clip left to fill it painted its waveform straight over them — so the

0 commit comments

Comments
 (0)