diff --git a/packages/studio/src/hooks/usePanelLayout.test.ts b/packages/studio/src/hooks/usePanelLayout.test.ts index 2106b02b6..b1ddf2fa1 100644 --- a/packages/studio/src/hooks/usePanelLayout.test.ts +++ b/packages/studio/src/hooks/usePanelLayout.test.ts @@ -2,23 +2,25 @@ import React, { act } from "react"; import { createRoot } from "react-dom/client"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { usePanelLayout } from "./usePanelLayout"; (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; afterEach(() => { document.body.innerHTML = ""; + vi.doUnmock("../components/editor/manualEditingAvailability"); + vi.resetModules(); }); -function renderPanelLayout() { +function renderPanelLayoutWith(hook: typeof usePanelLayout) { const host = document.createElement("div"); document.body.append(host); const root = createRoot(host); let current: ReturnType | null = null; function Harness() { - current = usePanelLayout(); + current = hook(); return null; } @@ -35,6 +37,10 @@ function renderPanelLayout() { }; } +function renderPanelLayout() { + return renderPanelLayoutWith(usePanelLayout); +} + describe("usePanelLayout — right inspector panes", () => { it("toggleRightInspectorPane independently flips one pane, allowing both open at once", () => { const harness = renderPanelLayout(); @@ -67,4 +73,46 @@ describe("usePanelLayout — right inspector panes", () => { harness.unmount(); }); + + it("setRightPanelTab additively opens a pane when the flat inspector is off (legacy behavior)", async () => { + vi.resetModules(); + vi.doMock("../components/editor/manualEditingAvailability", async () => { + const actual = await vi.importActual< + typeof import("../components/editor/manualEditingAvailability") + >("../components/editor/manualEditingAvailability"); + return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: false }; + }); + const { usePanelLayout: usePanelLayoutFlatOff } = await import("./usePanelLayout"); + const harness = renderPanelLayoutWith(usePanelLayoutFlatOff); + expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true }); + + act(() => harness.getState().setRightPanelTab("layers")); + // Legacy (split-view) behavior: additive, both panes end up open. + expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: true }); + + harness.unmount(); + }); + + it("setRightPanelTab is flat-aware: exclusivity holds for callers other than a direct in-panel tab click", async () => { + vi.resetModules(); + vi.doMock("../components/editor/manualEditingAvailability", async () => { + const actual = await vi.importActual< + typeof import("../components/editor/manualEditingAvailability") + >("../components/editor/manualEditingAvailability"); + return { ...actual, STUDIO_FLAT_INSPECTOR_ENABLED: true }; + }); + const { usePanelLayout: usePanelLayoutFlatOn } = await import("./usePanelLayout"); + const harness = renderPanelLayoutWith(usePanelLayoutFlatOn); + expect(harness.getState().rightInspectorPanes).toEqual({ layers: false, design: true }); + + // Element-select / block-params-close / header Inspector-button callers + // all reach setRightPanelTab directly, not through the in-panel tab + // click's own setExclusiveRightInspectorPane call — this must still + // enforce exclusivity under the flat flag, or both tabs end up + // highlighted while only one pane actually renders. + act(() => harness.getState().setRightPanelTab("layers")); + expect(harness.getState().rightInspectorPanes).toEqual({ layers: true, design: false }); + + harness.unmount(); + }); }); diff --git a/packages/studio/src/hooks/usePanelLayout.ts b/packages/studio/src/hooks/usePanelLayout.ts index 2e0420e72..df94b244e 100644 --- a/packages/studio/src/hooks/usePanelLayout.ts +++ b/packages/studio/src/hooks/usePanelLayout.ts @@ -6,6 +6,7 @@ import type { } from "../utils/studioHelpers"; import { readStudioUiPreferences, writeStudioUiPreferences } from "../utils/studioUiPreferences"; import { trackStudioEvent } from "../utils/studioTelemetry"; +import { STUDIO_FLAT_INSPECTOR_ENABLED } from "../components/editor/manualEditingAvailability"; export interface InitialPanelLayoutState { rightCollapsed?: boolean | null; @@ -80,7 +81,20 @@ export function usePanelLayout(initialState?: InitialPanelLayoutState) { const trackedSetRightPanelTab = useCallback( (tab: RightPanelTab) => { if (tab === "design" || tab === "layers") { - setRightInspectorPanes((panes) => ({ ...panes, [tab]: true })); + // Flat inspector: Layers always renders full-height by itself (see + // StudioRightPanel's render gate), so this MUST land on the same + // radio-style exclusivity setExclusiveRightInspectorPane enforces for + // the direct in-panel tab click — every OTHER path that reaches here + // (element select, closing block-params, the header Inspector + // button, and this function's own callers outside an active + // inspector tab) would otherwise additively leave both panes `true` + // and reproduce the "both tabs highlight, only one renders" bug this + // still-additive branch used to cause under the flat flag. + setRightInspectorPanes( + STUDIO_FLAT_INSPECTOR_ENABLED + ? { design: tab === "design", layers: tab === "layers" } + : (panes) => ({ ...panes, [tab]: true }), + ); } setRightPanelTab(tab); trackStudioEvent("tab_switch", { panel: "right_panel", tab });