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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
This commit is contained in:
2026-08-28 11:19:30 +02:00
co-authored by Claude Opus 5
parent e18a669aa0
commit 7e8fd1182e
2 changed files with 37 additions and 0 deletions
@@ -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 (
<LookProvider dashboard={draft}>
{/* 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. */}
<div
className={cn(
"absolute inset-0 overflow-hidden px-4 pb-24 pt-20 transition-[padding] duration-200",
panelOpen && "md:pr-[27rem]",
)}
data-testid="dashboard-canvas"
onClick={(event) => {
const target = event.target as Element
if (!event.currentTarget.contains(target)) return
if (target.closest(ON_WIDGET)) return
setSelected(null)
setSettingsOpen(false)
}}
>
<PaletteProvider dashboard={draft}>{body}</PaletteProvider>
</div>