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, }) => {