fix(keymap): open shortcut menu when activating an action (#2148)

Double-clicking an action in Settings > Keymap went straight to the Keyboard Shortcut dialog, where mouse clicks do nothing, so trying to bind Alt+Click there got no reaction. Add Mouse Shortcut was reachable only by right-click.

Double-click and Enter now open the same menu as right-click, at the pointer or the selected row.

Sibling covered: the keyboard dialog no longer inherits mouse recording mode from an earlier Add Mouse Shortcut.

Fixes #2126
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-08 20:53:51 +08:00
1 джерело 278936dd7d
коміт 9a4ef162bb
3 files changed
+118 -4

No files matched your search

+41
Переглянути файл
@@ -0,0 +1,41 @@
import { expect, test } from "./fixtures";
for (const activation of ["double-click", "Enter"] as const) {
test(`Keymap ${activation} offers mouse and keyboard shortcuts`, async ({ workbench: page }) => {
await page.keyboard.press("Meta+,");
const settings = page.getByRole("dialog", { name: "Settings", exact: true });
await settings.getByRole("treeitem", { name: "Keymap", exact: true }).click();
await settings.getByRole("textbox", { name: "Search actions", exact: true }).fill("Find Action");
const row = settings.getByRole("tree", { name: "Actions" }).getByRole("treeitem", { name: /Find Action/ });
const activate = async () => {
if (activation === "double-click") await row.dblclick();
else {
await row.click();
await page.keyboard.press("Enter");
}
};
await activate();
const keyboard = page.getByRole("menuitem", { name: "Add Keyboard Shortcut", exact: true });
await expect(keyboard).toBeFocused();
await page.keyboard.press("ArrowDown");
await expect(page.getByRole("menuitem", { name: "Add Mouse Shortcut", exact: true })).toBeFocused();
await page.keyboard.press("Enter");
const mouseDialog = page.getByRole("dialog", { name: "Mouse Shortcut: Find Action…", exact: true });
await mouseDialog.getByRole("textbox", { name: "Mouse shortcut" }).click({ modifiers: ["Alt"] });
await expect(mouseDialog.getByRole("button", { name: "OK", exact: true })).toBeEnabled();
await mouseDialog.getByRole("button", { name: "OK", exact: true }).click();
await page
.getByRole("dialog", { name: "Warning", exact: true })
.getByRole("button", { name: "Leave", exact: true })
.click();
await expect(row).toContainText("Click");
await activate();
await keyboard.click();
const keyboardDialog = page.getByRole("dialog", { name: "Keyboard Shortcut: Find Action…", exact: true });
await expect(keyboardDialog.getByRole("textbox", { name: "First stroke" })).toBeFocused();
await keyboardDialog.getByRole("button", { name: "Cancel", exact: true }).click();
await row.click({ button: "right" });
await expect(keyboard).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Add Mouse Shortcut", exact: true })).toBeVisible();
});
}
+66 -1
Переглянути файл
@@ -25,7 +25,7 @@ import { registerAction, executeAction } from "@/platform/actions";
import { usePlatform } from "@/platform/platformInfo";
import { useProject } from "@/platform/project";
import { useKeymap } from "@/platform/keymap";
import { ContextMenuHost } from "@/ui";
import { ContextMenuHost, hideContextMenu, useContextMenuStore } from "@/ui";
import { PageContext, useSetting } from "../components/context";
import { DEFAULT_SETTINGS, setAt } from "../model/schema";
import { openSettings, stage, useSettingsDialog } from "../model/staging";
@@ -53,6 +53,7 @@ beforeEach(() => {
afterEach(() => {
act(() => useSettingsDialog.setState({ open: false, projectStructureOpen: false }));
act(hideContextMenu);
dispose();
});
@@ -328,8 +329,10 @@ describe("Settings dialog", () => {
});
registerAction({ id: "t.mine", title: "My Action", category: "Code", when: "editorFocus", run: () => {} });
const dialog = await openDialog("keymap");
render(<ContextMenuHost />);
const row = within(dialog).getByText("My Action");
fireEvent.doubleClick(row);
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" }));
const recorder = await screen.findByRole("dialog", { name: /Keyboard Shortcut: My Action/ });
const first = within(recorder).getByRole("textbox", { name: "First stroke" });
fireEvent.keyDown(first, { code: "KeyD", key: "d", metaKey: true });
@@ -349,6 +352,67 @@ describe("Settings dialog", () => {
expect(useSettingsDialog.getState().open).toBe(false);
});
it.each(["double-click", "Enter"])(
"opens the shortcut menu on %s and clears mouse recording mode",
async (activation) => {
render(<ContextMenuHost />);
registerAction({ id: "t.activate", title: "Activation Target", category: "Code", run: () => {} });
const dialog = await openDialog("keymap");
const row = within(dialog).getByText("Activation Target").closest('[role="treeitem"]')!;
const tree = within(dialog).getByRole("tree", { name: "Actions" });
const rect = vi.spyOn(row, "getBoundingClientRect").mockReturnValue({
left: 100,
bottom: 150,
top: 126,
right: 500,
width: 400,
height: 24,
x: 100,
y: 126,
toJSON: () => ({}),
});
const activateRow = () => {
fireEvent.mouseDown(row, { button: 0 });
if (activation === "double-click") fireEvent.doubleClick(row, { clientX: 220, clientY: 138 });
else fireEvent.keyDown(tree, { key: "Enter" });
};
activateRow();
expect(useContextMenuStore.getState().menu).toMatchObject(
activation === "double-click" ? { x: 220, y: 138 } : { x: 100, y: 150 },
);
expect(screen.queryByRole("dialog", { name: /Shortcut: Activation Target/ })).not.toBeInTheDocument();
const keyboard = await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" });
expect(keyboard).toHaveFocus();
fireEvent.keyDown(keyboard, { key: "ArrowDown" });
const mouse = screen.getByRole("menuitem", { name: "Add Mouse Shortcut" });
expect(mouse).toHaveFocus();
fireEvent.keyDown(mouse, { key: "Enter" });
const recorder = await screen.findByRole("dialog", { name: "Mouse Shortcut: Activation Target" });
fireEvent.mouseDown(within(recorder).getByRole("textbox", { name: "Mouse shortcut" }), {
button: 0,
altKey: true,
});
fireEvent.click(within(recorder).getByRole("button", { name: "OK" }));
const overrides = useSettingsDialog.getState().staged["global|keymapOverrides"]?.value;
expect(overrides).toEqual({ "t.activate": ["Alt+Button1"] });
activateRow();
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" }));
const keys = await screen.findByRole("dialog", { name: "Keyboard Shortcut: Activation Target" });
expect(within(keys).getByRole("textbox", { name: "First stroke" })).toBeInTheDocument();
fireEvent.click(within(keys).getByRole("button", { name: "Cancel" }));
fireEvent.contextMenu(row);
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Mouse Shortcut" }));
fireEvent.click(
within(await screen.findByRole("dialog", { name: "Mouse Shortcut: Activation Target" })).getByRole("button", {
name: "Cancel",
}),
);
fireEvent.click(within(dialog).getByRole("button", { name: "Add Keyboard Shortcut" }));
expect(await screen.findByRole("dialog", { name: "Keyboard Shortcut: Activation Target" })).toBeInTheDocument();
rect.mockRestore();
},
);
it("duplicates the keymap from the gear menu and edits the copy", async () => {
render(<ContextMenuHost />);
registerAction({ id: "t.dup", title: "Dup Target", category: "Code", run: () => {} });
@@ -364,6 +428,7 @@ describe("Settings dialog", () => {
"custom.macos-copy": { name: "macOS copy", parent: "intellij-macos", overrides: {} },
});
fireEvent.doubleClick(within(dialog).getByText("Dup Target"));
fireEvent.click(await screen.findByRole("menuitem", { name: "Add Keyboard Shortcut" }));
const recorder = await screen.findByRole("dialog", { name: /Keyboard Shortcut: Dup Target/ });
fireEvent.keyDown(within(recorder).getByRole("textbox", { name: "First stroke" }), {
code: "F9",
+11 -3
Переглянути файл
@@ -2,8 +2,8 @@
* Keymap page (IDEA layout): keymap selector with a gear menu (Duplicate… /
* Rename… / Restore Defaults / Delete), search by name or by recorded shortcut,
* and the action tree grouped as Main Menu › File…Help, Editor Actions, Tool
* Windows, Other, Plugins. Add / Remove / Reset via the context menu (Enter /
* double-click adds). Edits are staged as the `keymap`, `keymapOverrides`
* Windows, Other, Plugins. Enter / double-click opens the context menu to add,
* remove or reset shortcuts. Edits are staged as the `keymap`, `keymapOverrides`
* (bundled keymaps) and `customKeymaps` (user keymaps) settings, global scope.
*/
import { useMemo, useRef, useState, type MouseEvent } from "react";
@@ -396,7 +396,15 @@ export function KeymapPage() {
multiSelect={false}
speedSearch={false}
onSelectionChange={(_sel, node) => setLead(node?.kind === "action" ? node.key : null)}
onActivate={(n) => n.kind === "action" && setRecordFor(n.action)}
onActivate={(n, e) => {
if (n.kind !== "action") return;
if ("clientX" in e) showContextMenu(e, menuFor(n.action));
else {
const row = document.getElementById(e.currentTarget.getAttribute("aria-activedescendant") ?? "");
const rect = row?.getBoundingClientRect();
if (rect) showContextMenu({ x: rect.left, y: rect.bottom }, menuFor(n.action));
}
}}
onContextMenu={(n, _sel, e: MouseEvent) => {
if (n?.kind !== "action") return;
e.preventDefault();