From 6054d4534376afa4deeb2caeb08e6786ca75e1d4 Mon Sep 17 00:00:00 2001 From: "clagentic-builder[bot]" <290147524+clagentic-builder[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:46:51 -0400 Subject: [PATCH] fix(diagnostics): stop panel from being unclickable and add outside-click dismiss (lr-b580) Root cause: #diagnostics-panel lives inside #info-panels, a fixed, viewport-spanning, pointer-events:none container that sibling panels (#usage-panel, #status-panel, #context-panel, #team-panel) opt back into via a shared pointer-events:auto selector. lr-8294 added the diagnostics panel to the DOM/CSS but never added it to that selector, so it inherited pointer-events:none -- the panel itself, including its own X button, became fully unclickable. Fixes: - menus.css: add #diagnostics-panel to the shared pointer-events:auto and .hidden selectors alongside its sibling panels. - diagnostics.js: add a document click-outside listener (mirroring the existing configPopover pattern in app-panels.js) so the panel is dismissable without a dedicated backdrop element, routed through the existing closePanelAndRefocus() helper so focus restoration stays consistent with the Escape-key and close-button paths. Regression coverage: test/diagnostics-panel-pointer-events-lr-b580.test.js (source-text assertions, matching the project's existing DOM-harness-free convention). npm test: 597 tests, 596 pass, 1 fail -- pre-existing process-level exit code quirk noted by andy on lr-1a26 (all individual TAP subtests report ok, including the 5 new ones added here). --- lib/public/css/menus.css | 4 +- lib/public/modules/diagnostics.js | 10 ++ ...stics-panel-pointer-events-lr-b580.test.js | 102 ++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 test/diagnostics-panel-pointer-events-lr-b580.test.js diff --git a/lib/public/css/menus.css b/lib/public/css/menus.css index 50a9746d..926e0522 100644 --- a/lib/public/css/menus.css +++ b/lib/public/css/menus.css @@ -669,7 +669,7 @@ #info-panels:empty { display: none; } /* --- Usage panel --- */ -#usage-panel, #status-panel, #context-panel, #team-panel { +#usage-panel, #status-panel, #context-panel, #team-panel, #diagnostics-panel { background: var(--bg-alt); border: 1px solid var(--border); border-radius: 12px; @@ -678,7 +678,7 @@ pointer-events: auto; } -#usage-panel.hidden, #status-panel.hidden, #context-panel.hidden, #team-panel.hidden { display: none; } +#usage-panel.hidden, #status-panel.hidden, #context-panel.hidden, #team-panel.hidden, #diagnostics-panel.hidden { display: none; } .usage-panel-header { display: flex; diff --git a/lib/public/modules/diagnostics.js b/lib/public/modules/diagnostics.js index 9690d2cf..93ad6aeb 100644 --- a/lib/public/modules/diagnostics.js +++ b/lib/public/modules/diagnostics.js @@ -69,6 +69,16 @@ export function initDiagnostics() { closePanelAndRefocus(); } }); + + // Click-outside dismiss: the panel is a non-modal surface (no backdrop + // element), so listen on the document and close when a click lands + // outside the panel and outside its topbar toggle button (lr-b580). + document.addEventListener("click", function (e) { + if (panelEl.classList.contains("hidden")) return; + if (panelEl.contains(e.target)) return; + if (panelToggleBtn && panelToggleBtn.contains(e.target)) return; + closePanelAndRefocus(); + }); } // ============================================================ diff --git a/test/diagnostics-panel-pointer-events-lr-b580.test.js b/test/diagnostics-panel-pointer-events-lr-b580.test.js new file mode 100644 index 00000000..9ebfdadd --- /dev/null +++ b/test/diagnostics-panel-pointer-events-lr-b580.test.js @@ -0,0 +1,102 @@ +// diagnostics-panel-pointer-events-lr-b580.test.js — lr-b580 regression coverage. +// +// Root cause: #diagnostics-panel lives inside #info-panels +// (lib/public/css/menus.css), which is `position: fixed; pointer-events: none` +// so it can span the viewport without blocking clicks on the app underneath. +// Sibling panels (#usage-panel, #status-panel, #context-panel, #team-panel) +// opt back into `pointer-events: auto` on their own bounds via a shared +// selector. lr-8294 added #diagnostics-panel to the DOM/CSS file but never +// added it to that selector, so the panel inherited `pointer-events: none` +// from its parent -- the panel itself (including its own X button) became +// unclickable, while clicks elsewhere in the app still worked (the panel was +// inert, not a click-trapping backdrop). +// +// Fix: add #diagnostics-panel to the shared pointer-events:auto selector, and +// add a click-outside-to-dismiss handler in diagnostics.js (the panel has no +// backdrop element, so dismissal is done via a document click listener that +// mirrors the existing configPopover outside-click pattern in +// app-panels.js), reusing the shared closePanelAndRefocus() helper so +// Escape-close+refocus stays consistent with all dismiss paths. +// +// These are source-text regression checks (no DOM harness required), matching +// the project's existing xss-escape.test.js / server-settings-mobile-parity +// convention for modules that only do DOM work inside functions. + +"use strict"; + +var test = require("node:test"); +var assert = require("node:assert/strict"); +var fs = require("fs"); +var path = require("path"); + +var MENUS_CSS = fs.readFileSync( + path.join(__dirname, "../lib/public/css/menus.css"), + "utf8" +); +var DIAGNOSTICS_JS = fs.readFileSync( + path.join(__dirname, "../lib/public/modules/diagnostics.js"), + "utf8" +); + +test("menus.css: #diagnostics-panel is included in the shared pointer-events:auto selector", () => { + // Isolate the selector block that follows the "Usage panel" comment so this + // assertion is scoped to the regression site. + var idx = MENUS_CSS.indexOf("/* --- Usage panel --- */"); + assert.ok(idx !== -1, 'expected "/* --- Usage panel --- */" comment block to exist in menus.css'); + var block = MENUS_CSS.slice(idx, idx + 400); + + assert.match( + block, + /#usage-panel,\s*#status-panel,\s*#context-panel,\s*#team-panel,\s*#diagnostics-panel\s*\{[^}]*pointer-events:\s*auto/, + "#diagnostics-panel must share the pointer-events:auto rule with its sibling panels -- without it, the panel inherits pointer-events:none from #info-panels and becomes fully unclickable (lr-b580 root cause)" + ); +}); + +test("menus.css: #diagnostics-panel.hidden is included in the shared display:none selector", () => { + assert.match( + MENUS_CSS, + /#usage-panel\.hidden,\s*#status-panel\.hidden,\s*#context-panel\.hidden,\s*#team-panel\.hidden,\s*#diagnostics-panel\.hidden\s*\{\s*display:\s*none;\s*\}/, + "#diagnostics-panel.hidden must be listed alongside its sibling panels for consistency with the shared panel pattern" + ); +}); + +test("diagnostics.js: initDiagnostics wires a click-outside-to-dismiss handler", () => { + var idx = DIAGNOSTICS_JS.indexOf("export function initDiagnostics"); + assert.ok(idx !== -1, "expected initDiagnostics to be exported"); + var block = DIAGNOSTICS_JS.slice(idx, idx + 2000); + + assert.match( + block, + /document\.addEventListener\(\s*["']click["']/, + "initDiagnostics must register a document-level click listener so the panel is dismissable by clicking outside it (lr-b580: the panel has no backdrop element to bind to)" + ); +}); + +test("diagnostics.js: the click-outside handler reuses closePanelAndRefocus (not a bare closePanel call)", () => { + var docClickIdx = DIAGNOSTICS_JS.indexOf('document.addEventListener("click"'); + assert.ok(docClickIdx !== -1, 'expected document.addEventListener("click", ...) to exist'); + var block = DIAGNOSTICS_JS.slice(docClickIdx, docClickIdx + 400); + + assert.match( + block, + /closePanelAndRefocus\s*\(\s*\)/, + "click-outside dismissal must go through closePanelAndRefocus() so focus is restored to the topbar toggle button, consistent with the Escape-key and close-button dismiss paths" + ); +}); + +test("diagnostics.js: click-outside handler ignores clicks on the panel itself and its toggle button", () => { + var docClickIdx = DIAGNOSTICS_JS.indexOf('document.addEventListener("click"'); + assert.ok(docClickIdx !== -1, 'expected document.addEventListener("click", ...) to exist'); + var block = DIAGNOSTICS_JS.slice(docClickIdx, docClickIdx + 400); + + assert.match( + block, + /panelEl\.contains\(e\.target\)/, + "handler must not dismiss when the click originated inside the panel itself (e.g. on the X button)" + ); + assert.match( + block, + /panelToggleBtn.*contains\(e\.target\)/, + "handler must not dismiss when the click originated on the topbar toggle button (that click already handles open/close via its own listener)" + ); +});