feat(studio): consolidate timeline editor callbacks

This commit is contained in:
Miguel Angel Simon Sierra
2026-07-27 19:52:06 +02:00
parent 555d7f7a5c
commit 2d1b905a56
14 changed files with 959 additions and 342 deletions
@@ -0,0 +1,93 @@
// @vitest-environment happy-dom
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import type { DomEditSelection } from "../components/editor/domEditing";
import type { RightInspectorPanes } from "../utils/studioHelpers";
import { makeSelection } from "./domSelectionTestHarness";
import { useInspectorState, type InspectorState } from "./useStudioContextValue";
interface HarnessProps {
rightPanelTab: string;
rightInspectorPanes: RightInspectorPanes;
rightCollapsed: boolean;
isPlaying: boolean;
isGestureRecording: boolean;
domEditSelection: DomEditSelection | null;
}
function renderInspectorState(props: HarnessProps): InspectorState {
let state: InspectorState | null = null;
function Harness() {
state = useInspectorState(
props.rightPanelTab,
props.rightInspectorPanes,
props.rightCollapsed,
props.isPlaying,
props.domEditSelection,
props.isGestureRecording,
);
return null;
}
renderToStaticMarkup(React.createElement(Harness));
if (!state) throw new Error("Expected inspector state");
return state;
}
function selectedProps(
overrides: Partial<HarnessProps> = {},
): HarnessProps & { domEditSelection: DomEditSelection } {
const element = document.createElement("div");
return {
rightPanelTab: "renders",
rightInspectorPanes: { layers: false, design: false },
rightCollapsed: true,
isPlaying: false,
isGestureRecording: false,
domEditSelection: makeSelection("Selected", element),
...overrides,
};
}
describe("useInspectorState", () => {
it("shows the motion path for pure selection with the inspector collapsed", () => {
expect(renderInspectorState(selectedProps()).shouldShowMotionPath).toBe(true);
});
it("hides the motion path without a selection", () => {
expect(
renderInspectorState({ ...selectedProps(), domEditSelection: null }).shouldShowMotionPath,
).toBe(false);
});
it("hides the motion path during playback", () => {
expect(renderInspectorState(selectedProps({ isPlaying: true })).shouldShowMotionPath).toBe(
false,
);
});
it("hides the motion path during gesture recording", () => {
expect(
renderInspectorState(selectedProps({ isGestureRecording: true })).shouldShowMotionPath,
).toBe(false);
});
it("keeps selected DOM bounds coupled to the inspector or variables panel", () => {
expect(renderInspectorState(selectedProps()).shouldShowSelectedDomBounds).toBe(false);
expect(
renderInspectorState(
selectedProps({
rightPanelTab: "design",
rightInspectorPanes: { layers: false, design: true },
}),
).shouldShowSelectedDomBounds,
).toBe(true);
expect(
renderInspectorState(selectedProps({ rightPanelTab: "variables" }))
.shouldShowSelectedDomBounds,
).toBe(true);
});
});
@@ -1,5 +1,6 @@
import { useCallback, useMemo, useRef, useState, type DragEvent } from "react";
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
import type { DomEditSelection } from "../components/editor/domEditing";
import type { StudioContextValue } from "../contexts/StudioContext";
import type { RightInspectorPanes } from "../utils/studioHelpers";
import type { TimelineFileDropHandler } from "./useTimelineEditingTypes";
@@ -69,6 +70,7 @@ export interface InspectorState {
designPanelActive: boolean;
inspectorPanelActive: boolean;
inspectorButtonActive: boolean;
shouldShowMotionPath: boolean;
shouldShowSelectedDomBounds: boolean;
}
@@ -77,6 +79,7 @@ export function useInspectorState(
rightInspectorPanes: RightInspectorPanes,
rightCollapsed: boolean,
isPlaying: boolean,
domEditSelection: DomEditSelection | null,
isGestureRecording?: boolean,
): InspectorState {
// fallow-ignore-next-line complexity
@@ -93,8 +96,9 @@ export function useInspectorState(
inspectorPanelActive,
inspectorButtonActive:
STUDIO_INSPECTOR_PANELS_ENABLED && !rightCollapsed && inspectorPanelActive,
// Keep the selection box + motion path drawn even when the Inspector is
// collapsed — closing the panel shouldn't visually deselect the element.
shouldShowMotionPath: !!domEditSelection && !isPlaying && !isGestureRecording,
// Keep the selection box drawn even when the Inspector is collapsed —
// closing the panel shouldn't visually deselect the element.
// The Variables tab also works against the canvas selection (bind card),
// so the selection outline stays visible there too.
shouldShowSelectedDomBounds:
@@ -102,7 +106,14 @@ export function useInspectorState(
!isPlaying &&
!isGestureRecording,
};
}, [rightPanelTab, rightInspectorPanes, rightCollapsed, isPlaying, isGestureRecording]);
}, [
rightPanelTab,
rightInspectorPanes,
rightCollapsed,
isPlaying,
isGestureRecording,
domEditSelection,
]);
}
// fallow-ignore-next-line complexity