Compare commits

...
Author SHA1 Message Date
Eric Lorenzo Benjamin Jr. f1763068e0 Treat contenteditable elements as text input for keyboard shortcuts (#53491)
canOverrideAlphanumericInput exempts textarea and the text-entry input types,
but has no branch for contenteditable, so an element the user is actively
typing into is still treated as fair game for global shortcuts. For an admin
that means a, c, d, e and m fire in the middle of a word.

The rest of the codebase already treats contenteditable as a typing surface
when it makes this decision, in ha-list-base and in the integrations
dashboard, and the HA-CODE-EDITOR tag name check in this same function is
that omission handled narrowly for CodeMirror, which is contenteditable
itself. Add the general check.

The HA-CODE-EDITOR entry is left in place. It is redundant in the common case
but it matches on the whole composed path rather than the event target, and
narrowing that belongs in its own change.

Adds tests for this function, which had none.
2026-10-13 14:10:59 +00:00
2 changed files with 84 additions and 0 deletions

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => {
if (
el.tagName === "TEXTAREA" ||
(el as HTMLElement).isContentEditable ||
el.parentElement?.tagName === "HA-SELECT" ||
el.parentElement?.tagName === "HA-DROPDOWN"
) {
@@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest";
import { canOverrideAlphanumericInput } from "../../../src/common/dom/can-override-input";
// jsdom does not implement isContentEditable — it is undefined on every element,
// including one carrying contenteditable="true". These stubs therefore describe
// the shape the function actually reads rather than using real elements.
const stub = (props: Record<string, unknown>) =>
props as unknown as EventTarget;
describe("canOverrideAlphanumericInput", () => {
it("allows shortcuts on a plain element", () => {
expect(canOverrideAlphanumericInput([stub({ tagName: "DIV" })])).toBe(true);
});
it("blocks shortcuts in a textarea", () => {
expect(canOverrideAlphanumericInput([stub({ tagName: "TEXTAREA" })])).toBe(
false
);
});
it("blocks shortcuts in a text input", () => {
expect(
canOverrideAlphanumericInput([stub({ tagName: "INPUT", type: "text" })])
).toBe(false);
});
it("allows shortcuts on inputs that are not typed into", () => {
for (const type of ["button", "checkbox", "hidden", "radio", "range"]) {
expect(
canOverrideAlphanumericInput([stub({ tagName: "INPUT", type })])
).toBe(true);
}
});
it("blocks shortcuts in a contenteditable element", () => {
expect(
canOverrideAlphanumericInput([
stub({ tagName: "DIV", isContentEditable: true }),
])
).toBe(false);
});
it("blocks shortcuts when a contenteditable element is the event target inside a shadow root", () => {
expect(
canOverrideAlphanumericInput([
stub({ tagName: "SPAN", isContentEditable: true }),
stub({ tagName: "DIV" }),
stub({ tagName: "MY-CARD" }),
])
).toBe(false);
});
it("blocks shortcuts inside a code editor", () => {
expect(
canOverrideAlphanumericInput([
stub({ tagName: "DIV" }),
stub({ tagName: "HA-CODE-EDITOR" }),
])
).toBe(false);
});
it("blocks shortcuts inside a menu", () => {
expect(
canOverrideAlphanumericInput([
stub({ tagName: "DIV" }),
stub({ tagName: "HA-MENU" }),
])
).toBe(false);
});
it("blocks shortcuts on a select or dropdown child", () => {
expect(
canOverrideAlphanumericInput([
stub({ tagName: "DIV", parentElement: { tagName: "HA-SELECT" } }),
])
).toBe(false);
expect(
canOverrideAlphanumericInput([
stub({ tagName: "DIV", parentElement: { tagName: "HA-DROPDOWN" } }),
])
).toBe(false);
});
});