fix(studio): commit lane edits through the edited element's own selection

An explicit null selection override now aborts the write instead of falling
back to domEditSelection: a caller that resolved a selection for its own
element and found none was committing onto whichever element happened to be
selected. Ease changes and the playhead keyframe toggle resolve the edited
element's animations and selection instead of the current selection's, and
lane header rows follow the real label-column width so a narrowed column no
longer hangs its value readout over the canvas.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-28 00:40:47 +02:00
parent 3181657147
commit c20c5366da
4 changed files with 28 additions and 5 deletions
@@ -313,7 +313,7 @@ export function useTimelineEditCallbacks({
: 0;
// Same frame for read and write: the toggled element's animations decide
// add-vs-remove, and its selection is what the mutation commits through.
const animations = resolveElementAnimations(el.key ?? el.id);
const animations = resolveElementAnimations(getTimelineElementIdentity(el));
void buildDomSelectionForTimelineElement(el).then((selection) => {
if (!selection) return;
const anim = animations.find((a) => a.keyframes);
@@ -163,3 +163,20 @@ describe("useGsapSelectionHandlers retime settlement", () => {
withSelection.unmount();
});
});
describe("useGsapSelectionHandlers selection override", () => {
it("aborts on an explicit null override instead of writing to the current selection", () => {
const removeKeyframe = vi.fn();
const rendered = renderHandlers(makeParams({ removeKeyframe }));
// Explicit null: the caller resolved a selection for its own element and
// found none, so the write must not land on the selected element.
rendered.handlers().handleGsapRemoveKeyframe("anim-1", 50, undefined, null);
expect(removeKeyframe).not.toHaveBeenCalled();
// Omitted override: falls back to the current selection as before.
rendered.handlers().handleGsapRemoveKeyframe("anim-1", 50);
expect(removeKeyframe).toHaveBeenCalledOnce();
rendered.unmount();
});
});
@@ -418,7 +418,7 @@ export function useGsapSelectionHandlers({
const handleGsapRemoveAllKeyframes = useCallback(
(animId: string, selectionOverride?: DomEditSelection | null) => {
const selection = selectionOverride ?? domEditSelection ?? lastSelectionRef.current;
const selection = resolveWriteSelection(selectionOverride);
if (!selection) return;
observeGsapMutation(
removeAllKeyframes(selection, animId),
@@ -427,7 +427,7 @@ export function useGsapSelectionHandlers({
"Remove all keyframes",
);
},
[domEditSelection, observeGsapMutation, removeAllKeyframes],
[resolveWriteSelection, observeGsapMutation, removeAllKeyframes],
);
const handleResetSelectedElementKeyframes = useCallback((): boolean => {
@@ -178,6 +178,7 @@ function PropertyGroupHeaderRow({
currentTime,
clipPercentage,
gutterBackground,
columnWidth,
onTogglePropertyGroupKeyframe,
onSeek,
}: {
@@ -188,6 +189,7 @@ function PropertyGroupHeaderRow({
currentTime: number;
clipPercentage: number;
gutterBackground: string;
columnWidth: number;
onTogglePropertyGroupKeyframe?: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
onSeek?: (time: number) => void;
}) {
@@ -201,10 +203,13 @@ function PropertyGroupHeaderRow({
<div
data-property-group={lane.group}
data-timeline-lane-top={getTimelineLaneTop(laneIndex)}
className="absolute left-0 flex items-center gap-1 px-1.5 text-[10px] text-white/65"
className="absolute left-0 flex items-center gap-1 overflow-hidden px-1.5 text-[10px] text-white/65"
style={{
top: getTimelineLaneTop(laneIndex),
width: LABEL_COL_W,
// The header column narrows to contentOrigin whenever that is under
// LABEL_COL_W; a lane row pinned to LABEL_COL_W then hangs its value
// readout over the canvas, on top of the clips it is labelling.
width: columnWidth,
height: LANE_H,
background: gutterBackground,
}}
@@ -339,6 +344,7 @@ export function TimelineTrackHeader({
currentTime={currentTime}
clipPercentage={clipPercentage}
gutterBackground={theme.gutterBackground}
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
onSeek={onSeek}
/>