From b2d11880490552ab6eb36f540b8d2fab0116ce0b Mon Sep 17 00:00:00 2001
From: arookieofc <2128194521@qq.com>
Date: Sat, 31 Oct 2026 21:43:20 +0800
Subject: [PATCH] fix(terminal): keep focus in the terminal after its context
menu closes (#566)
Right-clicking the terminal did not focus it, and closing any context menu sent focus to the editor. Popup recorded the element to restore in an effect, after Menu had already focused its first item, so it restored the menu item; focus then fell to the body and the workbench fallback focused the editor.
Popup now records the focused element while rendering, and the terminal takes focus when its context menu opens. Enter after the menu closes goes to the shell. This covers every context menu: focus returns to what held it before the menu opened.
Co-authored-by: a-rookie-of-C-language <186315048+a-rookie-of-C-language@users.noreply.github.com>
---
src/features/terminal/TerminalView.tsx | 3 +++
src/ui/Popup.test.tsx | 17 ++++++++++++++++-
src/ui/Popup.tsx | 14 ++++++++++++--
3 files changed, 31 insertions(+), 3 deletions(-)
diff --git a/src/features/terminal/TerminalView.tsx b/src/features/terminal/TerminalView.tsx
index f805b310d..6b35883ab 100644
--- a/src/features/terminal/TerminalView.tsx
+++ b/src/features/terminal/TerminalView.tsx
@@ -86,6 +86,9 @@ function paneMenu(e: MouseEvent, key: string) {
e.preventDefault();
const session = getSession(key);
if (!session) return;
+ // Right-clicking focuses the terminal, like IDEA; otherwise the menu closing
+ // restores focus to whatever was focused before it, often the editor.
+ session.focus();
const exited = session.state.status === "exited" || session.state.status === "error";
const mac = usePlatform.getState().os === "macos";
const items: MenuItem[] = [
diff --git a/src/ui/Popup.test.tsx b/src/ui/Popup.test.tsx
index b135322f8..ed98b0396 100644
--- a/src/ui/Popup.test.tsx
+++ b/src/ui/Popup.test.tsx
@@ -4,7 +4,7 @@ import { act, fireEvent, render, screen } from "@testing-library/react";
import { getContext, setContext } from "@/platform/context";
import { registerAction } from "@/platform/actions";
import { Popup } from "./Popup";
-import { Menu, MenuList } from "./Menu";
+import { ContextMenuHost, Menu, MenuList, hideContextMenu, showContextMenu } from "./Menu";
import { Dialog } from "./Dialog";
function Harness({ onClose }: { onClose?: (r: string) => void }) {
@@ -177,6 +177,21 @@ describe("Dialog initial focus", () => {
});
describe("Menu action enablement", () => {
+ it("returns focus to the element focused before a context menu opened", () => {
+ render(
+ <>
+
+
+ >,
+ );
+ const input = screen.getByRole("textbox", { name: "input" });
+ input.focus();
+ act(() => showContextMenu({ x: 0, y: 0 }, [{ label: "Paste", run: () => undefined }]));
+ expect(screen.getByRole("menuitem", { name: /Paste/ })).toHaveFocus();
+ act(() => hideContextMenu());
+ expect(input).toHaveFocus();
+ });
+
it("follows context changes after the menu opened", () => {
const dispose = registerAction({ id: "test.menuWhen", title: "Needs Flag", when: "qaMenuFlag", run: () => {} });
setContext("qaMenuFlag", false);
diff --git a/src/ui/Popup.tsx b/src/ui/Popup.tsx
index 7bb6b266f..31bde957f 100644
--- a/src/ui/Popup.tsx
+++ b/src/ui/Popup.tsx
@@ -3,7 +3,16 @@
* window-deactivation dismissal (top-most popup only for Escape), optional
* focus trap and focus restore.
*/
-import { useEffect, useLayoutEffect, useMemo, useRef, type CSSProperties, type ReactNode, type RefObject } from "react";
+import {
+ useEffect,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+ type CSSProperties,
+ type ReactNode,
+ type RefObject,
+} from "react";
import { createPortal } from "react-dom";
import clsx from "clsx";
import {
@@ -108,10 +117,11 @@ function PopupInner({
useLayoutEffect(() => {
onCloseRef.current = onClose;
});
+ // Read while rendering: content such as a menu focuses itself before any effect here runs.
+ const [previous] = useState(() => document.activeElement as HTMLElement | null);
useEffect(() => {
const layer = openLayer(() => elRef.current);
- const previous = document.activeElement as HTMLElement | null;
const onKey = (e: KeyboardEvent) => {
if (!layer.isTop()) return;