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;