From c264cf266f1c99f01f864587fcbb3e1dcc8ae763 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sat, 25 Jul 2026 22:57:20 +0200 Subject: [PATCH] fix(studio): stop lane-header clicks from reaching the track row The prev/next keyframe chevrons and the group toggle diamond let their click bubble to the ancestor track row, so seeking to a keyframe also reselected the track. The disclosure caret and the eye already stop it; these now match. Truncated labels (layer name, track label, group label, value readout) also carry a title so the full text is reachable on hover. --- .../player/components/LayerDisclosureRow.tsx | 4 ++- .../components/TimelineTrackHeader.test.tsx | 31 +++++++++++++++++++ .../player/components/TimelineTrackHeader.tsx | 30 ++++++++++++++---- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/packages/studio/src/player/components/LayerDisclosureRow.tsx b/packages/studio/src/player/components/LayerDisclosureRow.tsx index b630482d6..c061da300 100644 --- a/packages/studio/src/player/components/LayerDisclosureRow.tsx +++ b/packages/studio/src/player/components/LayerDisclosureRow.tsx @@ -50,7 +50,9 @@ export function LayerDisclosureRow({ > ◇ - {name} + + {name} + ); } diff --git a/packages/studio/src/player/components/TimelineTrackHeader.test.tsx b/packages/studio/src/player/components/TimelineTrackHeader.test.tsx index 57d6fb127..094b54a5f 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.test.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.test.tsx @@ -167,6 +167,37 @@ describe("TimelineTrackHeader", () => { act(() => view.root.unmount()); }); + // The lane header sits inside the track row, whose own click handler selects + // the track. Every control in the label column has to own its click, or + // seeking to a keyframe also reselects whatever is behind the header. + it("keeps lane-header control clicks off the ancestor track row", () => { + const onAncestorClick = vi.fn(); + const view = renderHeader({ + currentTime: 1, + onSeek: vi.fn(), + onTogglePropertyGroupKeyframe: vi.fn(), + }); + // React 18 delegates from the root container, so an ancestor of it is where + // a leaked click actually shows up. + document.body.addEventListener("click", onAncestorClick); + + // Every control in the lane's label column, found by row rather than by + // label, so a wording change to one button can't silently drop it here. + const controls = view.host.querySelectorAll( + '[data-property-group="position"] button', + ); + expect(controls.length).toBeGreaterThanOrEqual(3); + for (const button of controls) { + act(() => { + button.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + } + + document.body.removeEventListener("click", onAncestorClick); + expect(onAncestorClick).not.toHaveBeenCalled(); + act(() => view.root.unmount()); + }); + it("fills the toggle diamond exactly at that group's keyframe", () => { const view = renderHeader({ currentTime: 0.5 }); const positionToggle = view.host.querySelector( diff --git a/packages/studio/src/player/components/TimelineTrackHeader.tsx b/packages/studio/src/player/components/TimelineTrackHeader.tsx index e27812aab..c0fac5b49 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.tsx @@ -197,7 +197,11 @@ function LegacyTrackHeader({ {isAudioTrack && (