From 2ec2e75d40fa87bb94cc72a49c91c89f91c53bc8 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:20:50 +0800 Subject: [PATCH] fix(vcs): run the merge editor's buttons on a mouse click on macOS (#3160) WebKit doesn't focus a clicked button, so focus moved to the editor area outside the merge editor and cleared the focused merge editor; Apply, Cancel, Accept Yours/Theirs and the toolbar actions then did nothing on a mouse click. The merge editor root is now focusable (tabindex -1) so a click inside it keeps focus there. --- e2e/mergeEditor.spec.ts | 17 +++++++++++++++++ src/features/vcs/MergeEditor.module.css | 2 ++ src/features/vcs/MergeEditor.tsx | 4 ++++ 3 files changed, 23 insertions(+) diff --git a/e2e/mergeEditor.spec.ts b/e2e/mergeEditor.spec.ts index c0e625900..f4f76f65a 100644 --- a/e2e/mergeEditor.spec.ts +++ b/e2e/mergeEditor.spec.ts @@ -122,6 +122,23 @@ test("Apply writes and stages unresolved text only after confirmation", async ({ expect((await writes(page))[0]!.args[2]).toEqual({ changes: [{ from: 0, to: 13, insert: BASE }] }); }); +test("footer buttons work when a click doesn't focus them, as in WebKit", async ({ page }) => { + await mountMerge(page); + await result(page).locator(".cm-content").click(); + // WebKit focuses the clicked button's nearest focusable ancestor, not the button. + await page + .locator("#merge-host") + .getByRole("button", { name: "Apply", exact: true }) + .evaluate((button) => { + let target = button.parentElement; + while (target && !target.hasAttribute("tabindex")) target = target.parentElement; + if (target) target.focus(); + else (document.activeElement as HTMLElement | null)?.blur(); + (button as HTMLButtonElement).click(); + }); + await expect(page.getByRole("dialog")).toContainText("Save and mark the file as resolved anyway?"); +}); + test("binary and unreadable versions show an error with no Apply controls", async ({ page }) => { await mountMerge(page, ["\0base", "\0ours", "\0theirs"]); await expect(page.locator("#merge-host")).toContainText("Binary files cannot be resolved in the merge editor."); diff --git a/src/features/vcs/MergeEditor.module.css b/src/features/vcs/MergeEditor.module.css index 947662af8..9c02e739b 100644 --- a/src/features/vcs/MergeEditor.module.css +++ b/src/features/vcs/MergeEditor.module.css @@ -5,6 +5,7 @@ display: flex; flex-direction: column; background: var(--bg-app); + outline: none; } .toolbar { flex: none; @@ -84,6 +85,7 @@ } .message { flex: 1; + outline: none; display: flex; flex-direction: column; align-items: center; diff --git a/src/features/vcs/MergeEditor.tsx b/src/features/vcs/MergeEditor.tsx index 9c7e66bed..ea6eabbba 100644 --- a/src/features/vcs/MergeEditor.tsx +++ b/src/features/vcs/MergeEditor.tsx @@ -541,6 +541,10 @@ export function MergeEditor({ input }: { input: EditorInput }) { }, [actionHost]); const focusProps = { + // WebKit doesn't focus a clicked button: focus then moves to the nearest focusable + // ancestor, which must be this editor for the footer and toolbar buttons to keep it the + // focused merge editor their actions run in. + tabIndex: -1, onFocusCapture: (e: React.FocusEvent) => { const target = e.target as Node; if (oursRef.current?.contains(target)) paneRef.current = "left";