This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new aaba5c37195 UI: Don't fire keyboard shortcuts while typing in a Monaco 
editor (#75684)
aaba5c37195 is described below

commit aaba5c371955502c93df7b7e8bbc00061a6b6bfc
Author: Arnav Singh <[email protected]>
AuthorDate: Tue Oct 27 08:19:16 2026 -0700

    UI: Don't fire keyboard shortcuts while typing in a Monaco editor (#75684)
    
    closes: #75661
    
    Signed-off-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);
 

Reply via email to