This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new 3928f61089a [v3-4-test] UI: Don't fire keyboard shortcuts while typing
in a Monaco editor (#75684) (#75797)
3928f61089a is described below
commit 3928f61089a75cc924946d0fd9b3f928e9fd3d20
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Oct 27 16:50:55 2026 -0400
[v3-4-test] UI: Don't fire keyboard shortcuts while typing in a Monaco
editor (#75684) (#75797)
closes: #75661
(cherry picked from commit aaba5c371955502c93df7b7e8bbc00061a6b6bfc)
Signed-off-by: Arnav Singh <[email protected]>
Co-authored-by: Arnav Singh <[email protected]>
---
.../src/airflow/ui/src/hooks/useShortcut.test.tsx | 64 +++++++++++++++++++++-
.../src/airflow/ui/src/hooks/useShortcut.ts | 16 +++++-
2 files changed, 77 insertions(+), 3 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx
b/airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx
index 8ec2c554e53..f66cce73abe 100644
--- a/airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx
+++ b/airflow-core/src/airflow/ui/src/hooks/useShortcut.test.tsx
@@ -18,8 +18,8 @@
*/
import type { PropsWithChildren } from "react";
-import { renderHook } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
+import { fireEvent, renderHook } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
import {
ShortcutRegistryProvider,
@@ -65,7 +65,45 @@ const renderShortcut = (initialEnabled = true) =>
{ initialProps: { enabled: initialEnabled }, wrapper },
);
+const renderKeyShortcut = (callback: () => void, ignoreEventWhen?: (event:
KeyboardEvent) => boolean) =>
+ renderHook(
+ () =>
+ useShortcut({
+ callback,
+ category: "logs",
+ descriptionKey: "shortcuts.descriptions.toggleWrap",
+ keys: "g",
+ options: ignoreEventWhen ? { ignoreEventWhen } : undefined,
+ }),
+ { wrapper },
+ );
+
+// Monaco's EditContext input is a plain <div>, not a form field.
+const createMonacoInput = () => {
+ const editor = document.createElement("div");
+ const input = document.createElement("div");
+
+ editor.className = "monaco-editor";
+ input.className = "native-edit-context";
+ input.tabIndex = 0;
+ editor.append(input);
+ document.body.append(editor);
+
+ return input;
+};
+
+const pressG = (target: Element) => {
+ fireEvent.keyDown(target, { code: "KeyG", key: "g" });
+ fireEvent.keyUp(target, { code: "KeyG", key: "g" });
+};
+
+const ignoreEverything = () => true;
+
describe("useShortcut", () => {
+ afterEach(() => {
+ document.body.innerHTML = "";
+ });
+
it("registers the shortcut while mounted", () => {
const { result } = renderShortcut();
@@ -109,4 +147,26 @@ describe("useShortcut", () => {
expect(result.current).toHaveLength(1);
expect(result.current[0]?.keys).toEqual(["shift+ArrowUp",
"shift+ArrowDown"]);
});
+
+ it("lets a key typed into a Monaco editor through instead of firing the
shortcut", () => {
+ const callback = vi.fn();
+
+ renderKeyShortcut(callback);
+ pressG(createMonacoInput());
+
+ expect(callback).not.toHaveBeenCalled();
+
+ pressG(document.body);
+
+ expect(callback).toHaveBeenCalledTimes(1);
+ });
+
+ it("still applies the caller's ignoreEventWhen", () => {
+ const callback = vi.fn();
+
+ renderKeyShortcut(callback, ignoreEverything);
+ pressG(document.body);
+
+ expect(callback).not.toHaveBeenCalled();
+ });
});
diff --git a/airflow-core/src/airflow/ui/src/hooks/useShortcut.ts
b/airflow-core/src/airflow/ui/src/hooks/useShortcut.ts
index 557b3f08b28..1511fd7bd3a 100644
--- a/airflow-core/src/airflow/ui/src/hooks/useShortcut.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/useShortcut.ts
@@ -23,6 +23,12 @@ import { useTranslation } from "react-i18next";
import { type ShortcutDefinition, useShortcutRegistry } from
"src/context/keyboardShortcuts";
+// Where the browser supports it, Monaco takes input through an EditContext
<div>
+// rather than a <textarea>, so react-hotkeys-hook's form-field check does not
see
+// it and a shortcut key would be swallowed instead of typed into the editor.
+const isFromMonacoEditor = (event: KeyboardEvent) =>
+ event.target instanceof Element && event.target.closest(".monaco-editor")
!== null;
+
type UseShortcutParams = {
readonly callback: HotkeyCallback;
readonly dependencies?: DependencyList;
@@ -51,7 +57,15 @@ export const useShortcut = ({
const { t: translate } = useTranslation();
const { register, unregister } = useShortcutRegistry();
- const ref = useHotkeys(keys, callback, options, dependencies);
+ const ref = useHotkeys(
+ keys,
+ callback,
+ {
+ ...options,
+ ignoreEventWhen: (event) => isFromMonacoEditor(event) ||
(options?.ignoreEventWhen?.(event) ?? false),
+ },
+ dependencies,
+ );
const description = translate(descriptionKey);