From 1be0aec808e96a9b04001411ca5b3352cecb2a89 Mon Sep 17 00:00:00 2001 From: Eldoon Nemar Date: Fri, 5 Jun 2026 08:44:37 -0400 Subject: [PATCH] fix(frontend): reliably restore row focus on panel close (#1602) fix for the focus-restore@800 E2E test that's currently failing on master (see runs 26990436988, 26986419081) Chromium headless is notorious for dropping synchronous or rAF-based focus restores when elements are hidden. By manually blurring the active element before hiding the panel, and staggering the focus restore with a setTimeout macrotask after the rAF, we ensure the focus call lands after the browser has completed all implicit focus resets and event handlers. Furthermore, dynamically evaluating the focus resolver directly inside the deferred focus attempt prevents the target element from becoming stale if a live WebSocket packet triggers a background table re-render in the intervening milliseconds. --- public/packets.js | 31 ++++++++++++++++++++++++++----- test-slideover-1056-e2e.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/public/packets.js b/public/packets.js index b2b230dc..2b2f366b 100644 --- a/public/packets.js +++ b/public/packets.js @@ -426,16 +426,37 @@ // Defer to next microtask + rAF so the focus call lands AFTER any // event-handler bookkeeping (e.g. an Escape keydown chain that would // otherwise see focus snap back to as the key event unwinds). - const target = toFocus; const tryFocus = function () { // Munger #1: bail if a newer open() has happened since close-time. if (openSeq !== seqAtClose) return; - if (document.body.contains(target)) { - try { target.focus(); } catch {} + let t = toFocus; + if (resolver) { + try { + const fresh = resolver(); + if (fresh) t = fresh; + } catch (_) {} + } + + // MINOR fix: don't steal focus if the user already focused another input + if (document.activeElement && + document.activeElement !== document.body && + document.activeElement !== t && + !panel.contains(document.activeElement)) { + return; + } + + if (t && document.body.contains(t)) { + try { t.focus({ preventScroll: true }); } catch (_) {} } }; - tryFocus(); - requestAnimationFrame(tryFocus); + + requestAnimationFrame(function () { + if (document.activeElement && panel.contains(document.activeElement)) { + try { document.activeElement.blur(); } catch (_) {} + } + tryFocus(); + setTimeout(tryFocus, 10); + }); } } diff --git a/test-slideover-1056-e2e.js b/test-slideover-1056-e2e.js index f5439a90..3581c4b5 100644 --- a/test-slideover-1056-e2e.js +++ b/test-slideover-1056-e2e.js @@ -428,6 +428,36 @@ const PAGES = [ assert(r.isActive, 'focus did NOT restore to originating row after Escape: ' + JSON.stringify(r)); }); + await step('focus-restore@800: re-renders while open still restore to new row instance', async () => { + const rowKey = await openPanelFromRow(); + + // Force a re-render of the table so the original DOM node is detached + await page.evaluate(() => { + if (typeof window.renderRows === 'function') { + window.renderRows(); + } + }); + + await page.keyboard.press('Escape'); + // Wait for renderRows() + post-rAF focus restore to settle. + await page.waitForFunction((key) => { + const esc = (window.CSS && CSS.escape) ? CSS.escape(key) : key; + const row = document.querySelector('#nodesTable tbody tr[data-value="' + esc + '"]'); + return !!row && document.activeElement === row; + }, rowKey, { timeout: 2000 }).catch(() => {}); + + const r = await page.evaluate((key) => { + const esc = (window.CSS && CSS.escape) ? CSS.escape(key) : key; + const row = document.querySelector('#nodesTable tbody tr[data-value="' + esc + '"]'); + return { + rowExists: !!row, + isActive: !!row && document.activeElement === row, + }; + }, rowKey); + assert(r.rowExists, 'originating row vanished from DOM after manual re-render'); + assert(r.isActive, 'focus did NOT restore to NEW row instance after Escape: ' + JSON.stringify(r)); + }); + // ------------------------------------------------------------------ // SKIP: tracked in #1172 — flaky in CI Chromium, see issue for repro. // X-click focus-restore is real and works locally; head-to-head with