From 7e8fd1182e1b6f202bf6f44e86aa47c41f83c7b7 Mon Sep 17 00:00:00 2001 From: stroblme Date: Fri, 28 Aug 2026 11:19:30 +0200 Subject: [PATCH] Put the dashboard panels away on a click off the widgets Mirrors the flow editor's onPaneClick: clicking the canvas margin or the empty surface closes the widget settings and the dashboard panel. Widget frames, grid resize handles and menus portalled out of the canvas keep it open. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk --- .../components/Dashboard/DashboardEditor.tsx | 22 +++++++++++++++++++ frontend/tests/editor.spec.ts | 15 +++++++++++++ 2 files changed, 37 insertions(+) diff --git a/frontend/src/components/Dashboard/DashboardEditor.tsx b/frontend/src/components/Dashboard/DashboardEditor.tsx index f66bf97..1b6a01e 100644 --- a/frontend/src/components/Dashboard/DashboardEditor.tsx +++ b/frontend/src/components/Dashboard/DashboardEditor.tsx @@ -144,6 +144,14 @@ const AUTOSAVE_MS = 800 const INTERACTIVE = "button, a, input, select, textarea, [role='switch'], [role='combobox'], [role='slider'], .react-resizable-handle, .widget-grip" +/** + * What a click may land on without counting as a click off the widgets: the + * widget itself, and the handles the grid draws around it — resizing a tile is + * still working on it. The grid item covers both; the frame is what a stacked + * dashboard has instead. + */ +const ON_WIDGET = "[data-testid=widget-frame], .react-grid-item" + function nextId(dashboard: DashboardDef_Output, type: string): string { const taken = new Set(widgetsOf(dashboard).map((widget) => widget.id)) let candidate = type @@ -524,12 +532,26 @@ export function DashboardEditor({ return ( + {/* Clicking off the widgets is how you put a panel away, as it is on the + flow pane — the dashboard is what you went back to look at. A menu a + widget portals to `body` lands outside this element but still bubbles + through React's tree, so only a press that landed in the subtree + counts. */} + {/* biome-ignore lint/a11y/useKeyWithClickEvents: Escape closes the panel; this is a pointer shortcut, not a control. */} + {/* biome-ignore lint/a11y/noStaticElementInteractions: see above. */}
{ + const target = event.target as Element + if (!event.currentTarget.contains(target)) return + if (target.closest(ON_WIDGET)) return + setSelected(null) + setSettingsOpen(false) + }} > {body}
diff --git a/frontend/tests/editor.spec.ts b/frontend/tests/editor.spec.ts index 23ee60c..9cc1a86 100644 --- a/frontend/tests/editor.spec.ts +++ b/frontend/tests/editor.spec.ts @@ -135,6 +135,21 @@ test("the drag handle moves a widget without selecting it", async ({ await expect(settings).toBeVisible() }) +test("clicking off the widgets puts the settings panel away", async ({ + page, +}) => { + await openEditor(page) + + const settings = page.getByTestId("widget-settings") + await page.getByTestId("widget-frame").filter({ hasText: "Top" }).click() + await expect(settings).toBeVisible() + + // The margin around the canvas is not a widget, the way the flow pane is not + // a node. + await page.getByTestId("dashboard-canvas").click({ position: { x: 8, y: 8 } }) + await expect(settings).toBeHidden() +}) + test("a widget without its title can still be dragged and picked", async ({ page, }) => {