fix(shortcuts): try to fix ime composition checks (#6851)

This commit is contained in:
Elian Doran 2025-09-07 11:17:35 +03:00 committed by GitHub
commit 145f89eded
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 86 additions and 2 deletions

View File

@ -1,5 +1,5 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import shortcuts, { keyMatches, matchesShortcut } from "./shortcuts.js";
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
// Mock utils module
vi.mock("./utils.js", () => ({
@ -320,4 +320,36 @@ describe("shortcuts", () => {
expect(event.preventDefault).not.toHaveBeenCalled();
});
});
describe('isIMEComposing', () => {
it('should return true when event.isComposing is true', () => {
const event = { isComposing: true, keyCode: 65 } as KeyboardEvent;
expect(isIMEComposing(event)).toBe(true);
});
it('should return true when keyCode is 229', () => {
const event = { isComposing: false, keyCode: 229 } as KeyboardEvent;
expect(isIMEComposing(event)).toBe(true);
});
it('should return true when both isComposing is true and keyCode is 229', () => {
const event = { isComposing: true, keyCode: 229 } as KeyboardEvent;
expect(isIMEComposing(event)).toBe(true);
});
it('should return false for normal keys', () => {
const event = { isComposing: false, keyCode: 65 } as KeyboardEvent;
expect(isIMEComposing(event)).toBe(false);
});
it('should return false when isComposing is undefined and keyCode is not 229', () => {
const event = { keyCode: 13 } as KeyboardEvent;
expect(isIMEComposing(event)).toBe(false);
});
it('should handle null/undefined events gracefully', () => {
expect(isIMEComposing(null as any)).toBe(false);
expect(isIMEComposing(undefined as any)).toBe(false);
});
});
});

View File

@ -40,6 +40,24 @@ for (let i = 1; i <= 19; i++) {
keyMap[`f${i}`] = [`F${i}`];
}
/**
* Check if IME (Input Method Editor) is composing
* This is used to prevent keyboard shortcuts from firing during IME composition
* @param e - The keyboard event to check
* @returns true if IME is currently composing, false otherwise
*/
export function isIMEComposing(e: KeyboardEvent): boolean {
// Handle null/undefined events gracefully
if (!e) {
return false;
}
// Standard check for composition state
// e.isComposing is true when IME is actively composing
// e.keyCode === 229 is a fallback for older browsers where 229 indicates IME processing
return e.isComposing || e.keyCode === 229;
}
function removeGlobalShortcut(namespace: string) {
bindGlobalShortcut("", null, namespace);
}
@ -68,6 +86,13 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
}
const e = evt as KeyboardEvent;
// Skip processing if IME is composing to prevent shortcuts from
// interfering with text input in CJK languages
if (isIMEComposing(e)) {
return;
}
if (matchesShortcut(e, keyboardShortcut)) {
e.preventDefault();
e.stopPropagation();

View File

@ -8,6 +8,7 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import attributeService from "../services/attributes.js";
import FindInText from "./find_in_text.js";
import FindInCode from "./find_in_code.js";
import { isIMEComposing } from "../services/shortcuts.js";
import FindInHtml from "./find_in_html.js";
import type { EventData } from "../components/app_context.js";
@ -162,6 +163,11 @@ export default class FindWidget extends NoteContextAwareWidget {
this.$replaceButton.on("click", () => this.replace());
this.$input.on("keydown", async (e) => {
// Skip processing during IME composition
if (isIMEComposing(e.originalEvent as KeyboardEvent)) {
return;
}
if ((e.metaKey || e.ctrlKey) && (e.key === "F" || e.key === "f")) {
// If ctrl+f is pressed when the findbox is shown, select the
// whole input to find

View File

@ -8,6 +8,7 @@ import "./note_title.css";
import { isLaunchBarConfig } from "../services/utils";
import appContext from "../components/app_context";
import branches from "../services/branches";
import { isIMEComposing } from "../services/shortcuts";
export default function NoteTitleWidget() {
const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext();
@ -78,6 +79,12 @@ export default function NoteTitleWidget() {
spacedUpdate.scheduleUpdate();
}}
onKeyDown={(e) => {
// Skip processing if IME is composing to prevent interference
// with text input in CJK languages
if (isIMEComposing(e)) {
return;
}
// Focus on the note content when pressing enter.
if (e.key === "Enter") {
e.preventDefault();

View File

@ -4,7 +4,7 @@ import linkService from "../services/link.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
import appContext from "../components/app_context.js";
import shortcutService from "../services/shortcuts.js";
import shortcutService, { isIMEComposing } from "../services/shortcuts.js";
import { t } from "../services/i18n.js";
import { Dropdown, Tooltip } from "bootstrap";
@ -180,6 +180,14 @@ export default class QuickSearchWidget extends BasicWidget {
if (utils.isMobile()) {
this.$searchString.keydown((e) => {
// Skip processing if IME is composing to prevent interference
// with text input in CJK languages
// Note: jQuery wraps the native event, so we access originalEvent
const originalEvent = e.originalEvent as KeyboardEvent;
if (originalEvent && isIMEComposing(originalEvent)) {
return;
}
if (e.which === 13) {
if (this.$dropdownMenu.is(":visible")) {
this.search(); // just update already visible dropdown

View File

@ -13,6 +13,7 @@ import attribute_parser, { Attribute } from "../../../services/attribute_parser"
import ActionButton from "../../react/ActionButton";
import { escapeQuotes, getErrorMessage } from "../../../services/utils";
import link from "../../../services/link";
import { isIMEComposing } from "../../../services/shortcuts";
import froca from "../../../services/froca";
import contextMenu from "../../../menus/context_menu";
import type { CommandData, FilteredCommandNames } from "../../../components/app_context";
@ -287,6 +288,11 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
ref={wrapperRef}
style="position: relative; padding-top: 10px; padding-bottom: 10px"
onKeyDown={(e) => {
// Skip processing during IME composition
if (isIMEComposing(e)) {
return;
}
if (e.key === "Enter") {
// allow autocomplete to fill the result textarea
setTimeout(() => save(), 100);