From 0d140065ae943549808b2b9d7f32c4303724be9e Mon Sep 17 00:00:00 2001 From: wanzirong Date: Sun, 16 Aug 2026 23:41:29 +0800 Subject: [PATCH 1/3] feat(ext): dismiss branch popovers and dialogs on blur Clicks in the editor/main area never reach a webview, so a click outside the popover didn't dismiss it. Blur is the only signal that focus left the webview, so treat it like a click-outside: branch menu, branch actions, and all custom dialogs now close on blur, matching JetBrains. Dialogs answer 'cancelled' on blur; destructive confirms still only act on the explicit OK. --- apps/extension/src/changes/commitView.ts | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/extension/src/changes/commitView.ts b/apps/extension/src/changes/commitView.ts index 6b1d604..5d3433a 100644 --- a/apps/extension/src/changes/commitView.ts +++ b/apps/extension/src/changes/commitView.ts @@ -3763,6 +3763,7 @@ export class CommitViewProvider branchPill.setAttribute("aria-expanded", "false"); document.removeEventListener("mousedown", onBranchDocDown, true); document.removeEventListener("keydown", onBranchKey, true); + window.removeEventListener("blur", onBranchBlur, true); } function closeBranchSubmenu() { if (branchSubmenu) { branchSubmenu.remove(); branchSubmenu = null; } @@ -3775,6 +3776,12 @@ export class CommitViewProvider if (inSub || inMenu || onPill) return; closeBranchMenu(); } + // Clicks in the editor/main area never reach this webview; blur is the only + // signal that focus left it, so close the popover (JetBrains dismisses + // popovers on any click anywhere in the IDE). + function onBranchBlur() { + closeBranchMenu(); + } function onBranchKey(e) { if (e.key === "Escape") { if (branchSubmenu) { closeBranchSubmenu(); subMenuFor = null; return; } @@ -3898,10 +3905,17 @@ export class CommitViewProvider if (actionMenuEl) { actionMenuEl.remove(); actionMenuEl = null; } document.removeEventListener("mousedown", onActionDocDown, true); document.removeEventListener("keydown", onActionKey, true); + window.removeEventListener("blur", onActionBlur, true); } function onActionDocDown(e) { if (actionMenuEl && !actionMenuEl.contains(e.target)) closeActionMenu(); } + // The webview cannot see clicks in the editor/main area — those never reach + // this document. Blur is the only signal that focus left the webview, so + // treat it like a click-outside (JetBrains dismisses popovers on any click). + function onActionBlur() { + closeActionMenu(); + } function onActionKey(e) { if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); closeActionMenu(); } } @@ -3938,6 +3952,7 @@ export class CommitViewProvider menu.style.top = Math.round(top) + "px"; document.addEventListener("mousedown", onActionDocDown, true); document.addEventListener("keydown", onActionKey, true); + window.addEventListener("blur", onActionBlur, true); const first = list.querySelector(".bm-subaction"); if (first) first.focus(); } @@ -4225,6 +4240,7 @@ export class CommitViewProvider var dlgEl = null; var dlgBackdrop = null; var dlgKeyHandler = null; + var dlgBlurHandler = null; var dlgReturnFocus = null; /** Correlation id of a host-requested dialog, so we can answer exactly once. */ var dlgHostId = null; @@ -4241,6 +4257,10 @@ export class CommitViewProvider window.removeEventListener("keydown", dlgKeyHandler, true); dlgKeyHandler = null; } + if (dlgBlurHandler) { + window.removeEventListener("blur", dlgBlurHandler, true); + dlgBlurHandler = null; + } if (dlgBackdrop) { dlgBackdrop.remove(); dlgBackdrop = null; } if (dlgEl) { dlgEl.remove(); dlgEl = null; } // Put focus back where it came from; otherwise it falls to and the @@ -4326,6 +4346,13 @@ export class CommitViewProvider }; window.addEventListener("keydown", dlgKeyHandler, true); + // Clicks in the editor/main area never reach this webview; blur is the + // only signal that focus left it, so dismiss the dialog like JetBrains + // does for any click outside. Closing answers "undefined" (cancelled) — + // safe for destructive confirms, which only act on the explicit OK. + dlgBlurHandler = function () { closeDialog(undefined); }; + window.addEventListener("blur", dlgBlurHandler, true); + dlgEl = el("div", "rp-panel"); dlgEl.setAttribute("role", "dialog"); dlgEl.setAttribute("aria-modal", "true"); @@ -4766,6 +4793,7 @@ export class CommitViewProvider setTimeout(() => { document.addEventListener("mousedown", onBranchDocDown, true); document.addEventListener("keydown", onBranchKey, true); + window.addEventListener("blur", onBranchBlur, true); }, 0); } branchPill.addEventListener("click", openBranchMenu); From 1af147cc30c3bd63f9b914860286cde0efe32c85 Mon Sep 17 00:00:00 2001 From: wanzirong Date: Mon, 17 Aug 2026 11:32:17 +0800 Subject: [PATCH 2/3] fix(ext): only dismiss on real focus loss, not internal focus moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blur does not bubble, but capture starts at window, so a click inside a menu or dialog (focus moving off the focused row/input) fired the dismiss handler too. Check document.hasFocus() in a setTimeout so a focus move that stays in the webview is not treated as a click outside, and guard on the element still existing so a stale callback cannot close a dialog that was already answered — or a newer one. --- apps/extension/src/changes/commitView.ts | 29 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/extension/src/changes/commitView.ts b/apps/extension/src/changes/commitView.ts index 5d3433a..7b3691a 100644 --- a/apps/extension/src/changes/commitView.ts +++ b/apps/extension/src/changes/commitView.ts @@ -3779,8 +3779,15 @@ export class CommitViewProvider // Clicks in the editor/main area never reach this webview; blur is the only // signal that focus left it, so close the popover (JetBrains dismisses // popovers on any click anywhere in the IDE). + // blur does NOT bubble, but capture starts at window — so clicking inside + // the menu (moving focus off the focused row) fires this too. Only close + // when focus has genuinely left the webview: the setTimeout lets the focus + // move settle first, and the branchMenu-null guard keeps a stale callback + // from closing a menu that was already dismissed. function onBranchBlur() { - closeBranchMenu(); + setTimeout(() => { + if (branchMenu && !document.hasFocus()) closeBranchMenu(); + }, 0); } function onBranchKey(e) { if (e.key === "Escape") { @@ -3914,7 +3921,9 @@ export class CommitViewProvider // this document. Blur is the only signal that focus left the webview, so // treat it like a click-outside (JetBrains dismisses popovers on any click). function onActionBlur() { - closeActionMenu(); + setTimeout(() => { + if (actionMenuEl && !document.hasFocus()) closeActionMenu(); + }, 0); } function onActionKey(e) { if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); closeActionMenu(); } @@ -4348,9 +4357,19 @@ export class CommitViewProvider // Clicks in the editor/main area never reach this webview; blur is the // only signal that focus left it, so dismiss the dialog like JetBrains - // does for any click outside. Closing answers "undefined" (cancelled) — - // safe for destructive confirms, which only act on the explicit OK. - dlgBlurHandler = function () { closeDialog(undefined); }; + // does for any click outside. BUT blur does not bubble — capture starts + // at window — so a click INSIDE the dialog (focus moving off the input + // onto a row) fires this handler too. Only dismiss when focus has really + // left the webview: the setTimeout lets the focus move settle, and the + // dlgEl guard keeps a stale callback from closing a dialog that was + // already answered — or a newer one opened by the very choice that closed + // this one. Closing answers "undefined" (cancelled); destructive confirms + // still only act on the explicit OK. + dlgBlurHandler = function () { + setTimeout(function () { + if (dlgEl && !document.hasFocus()) closeDialog(undefined); + }, 0); + }; window.addEventListener("blur", dlgBlurHandler, true); dlgEl = el("div", "rp-panel"); From e7d4587aa61347f867a0ab66f51ee8758e61a8bf Mon Sep 17 00:00:00 2001 From: wanzirong Date: Thu, 20 Aug 2026 21:51:03 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(ext):=20dialogs=20dismiss=20on=20Escape?= =?UTF-8?q?/backdrop=20only=20=E2=80=94=20blur=20just=20closes=20popovers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blur also fires when switching to another app, so a half-typed ref name or branch name was lost. Branch menu and action menu keep blur dismissal (popovers match JetBrains), but the dialogs keep typed input across an alt-tab: Escape and backdrop-click are the only close paths again. Co-Authored-By: Claude --- apps/extension/src/changes/commitView.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/apps/extension/src/changes/commitView.ts b/apps/extension/src/changes/commitView.ts index 7b3691a..fcf90fa 100644 --- a/apps/extension/src/changes/commitView.ts +++ b/apps/extension/src/changes/commitView.ts @@ -4249,7 +4249,6 @@ export class CommitViewProvider var dlgEl = null; var dlgBackdrop = null; var dlgKeyHandler = null; - var dlgBlurHandler = null; var dlgReturnFocus = null; /** Correlation id of a host-requested dialog, so we can answer exactly once. */ var dlgHostId = null; @@ -4266,10 +4265,6 @@ export class CommitViewProvider window.removeEventListener("keydown", dlgKeyHandler, true); dlgKeyHandler = null; } - if (dlgBlurHandler) { - window.removeEventListener("blur", dlgBlurHandler, true); - dlgBlurHandler = null; - } if (dlgBackdrop) { dlgBackdrop.remove(); dlgBackdrop = null; } if (dlgEl) { dlgEl.remove(); dlgEl = null; } // Put focus back where it came from; otherwise it falls to and the @@ -4355,23 +4350,6 @@ export class CommitViewProvider }; window.addEventListener("keydown", dlgKeyHandler, true); - // Clicks in the editor/main area never reach this webview; blur is the - // only signal that focus left it, so dismiss the dialog like JetBrains - // does for any click outside. BUT blur does not bubble — capture starts - // at window — so a click INSIDE the dialog (focus moving off the input - // onto a row) fires this handler too. Only dismiss when focus has really - // left the webview: the setTimeout lets the focus move settle, and the - // dlgEl guard keeps a stale callback from closing a dialog that was - // already answered — or a newer one opened by the very choice that closed - // this one. Closing answers "undefined" (cancelled); destructive confirms - // still only act on the explicit OK. - dlgBlurHandler = function () { - setTimeout(function () { - if (dlgEl && !document.hasFocus()) closeDialog(undefined); - }, 0); - }; - window.addEventListener("blur", dlgBlurHandler, true); - dlgEl = el("div", "rp-panel"); dlgEl.setAttribute("role", "dialog"); dlgEl.setAttribute("aria-modal", "true");