fix: avoid shortcut conflicts in CodeMirror editors (#5224)

Prevents `alt+up` and `alt+down` from triggering global keybindings when focus is in CodeMirror editors or other typable elements.
This commit is contained in:
Sharad Saha 2025-07-10 16:23:41 +05:30 committed by GitHub
parent 0b605fe9cb
commit bf3e135679
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 97 additions and 1 deletions

View file

@ -1,7 +1,7 @@
import { onBeforeUnmount, onMounted } from "vue"
import { HoppActionWithOptionalArgs, invokeAction } from "./actions"
import { isAppleDevice } from "./platformutils"
import { isDOMElement, isTypableElement } from "./utils/dom"
import { isCodeMirrorEditor, isDOMElement, isTypableElement } from "./utils/dom"
import { getKernelMode } from "@hoppscotch/kernel"
import { listen } from "@tauri-apps/api/event"
@ -193,6 +193,15 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
return null
}
// Restrict alt+up and alt+down when the target is a codemirror editor
if (
modifierKey === "alt" &&
(key === "up" || key === "down") &&
isCodeMirrorEditor(target)
) {
return null
}
return `${modifierKey}-${key}`
}

View file

@ -0,0 +1,77 @@
import { describe, expect, test } from "vitest"
import { isDOMElement, isTypableElement, isCodeMirrorEditor } from "../dom"
describe("isDOMElement", () => {
test("returns true for valid HTMLElement", () => {
const div = document.createElement("div")
expect(isDOMElement(div)).toBe(true)
})
test("returns false for non-HTMLElement inputs", () => {
expect(isDOMElement(null)).toBe(false)
expect(isDOMElement(undefined)).toBe(false)
expect(isDOMElement("div")).toBe(false)
expect(isDOMElement(123)).toBe(false)
expect(isDOMElement({})).toBe(false)
})
})
describe("isTypableElement", () => {
test("returns true for enabled <input>", () => {
const input = document.createElement("input")
expect(isTypableElement(input)).toBe(true)
})
test("returns false for disabled <input>", () => {
const input = document.createElement("input")
input.disabled = true
expect(isTypableElement(input)).toBe(false)
})
test("returns true for enabled <textarea>", () => {
const textarea = document.createElement("textarea")
expect(isTypableElement(textarea)).toBe(true)
})
test("returns false for disabled <textarea>", () => {
const textarea = document.createElement("textarea")
textarea.disabled = true
expect(isTypableElement(textarea)).toBe(false)
})
test("returns true for contentEditable element", () => {
const div = document.createElement("div")
div.isContentEditable = true
expect(isTypableElement(div)).toBe(true)
})
test("returns false for regular non-typable div", () => {
const div = document.createElement("div")
expect(isTypableElement(div)).toBe(false)
})
})
describe("isCodeMirrorEditor", () => {
test("returns true if element is inside .cm-editor", () => {
const wrapper = document.createElement("div")
wrapper.classList.add("cm-editor")
const child = document.createElement("div")
wrapper.appendChild(child)
document.body.appendChild(wrapper)
expect(isCodeMirrorEditor(child)).toBe(true)
})
test("returns false if element is not inside .cm-editor", () => {
const div = document.createElement("div")
document.body.appendChild(div)
expect(isCodeMirrorEditor(div)).toBe(false)
})
test("returns false if input is not an HTMLElement", () => {
expect(isCodeMirrorEditor(null)).toBe(false)
expect(isCodeMirrorEditor("not-an-element")).toBe(false)
})
})

View file

@ -17,3 +17,13 @@ export function isTypableElement(el: HTMLElement): boolean {
return false
}
/**
* Checks if an element is a CodeMirror editor.
* @param el The element to check. If this is not an HTMLElement, the function will return false.
* @returns True if the element is a CodeMirror editor, false otherwise.
*/
export function isCodeMirrorEditor(el: EventTarget | null): boolean {
if (!(el instanceof HTMLElement)) return false
return el.closest(".cm-editor") !== null
}