From ee5578551d2d7649277b620149be7ad46ab2a94d Mon Sep 17 00:00:00 2001 From: mahirhir Date: Wed, 7 Oct 2026 07:45:42 +0900 Subject: [PATCH] useHotkeys: ignore keys that belong to an IME composition The Enter that confirms an IME conversion reached registered hotkeys, so Autocomplete selected the highlighted option and called onEnter with unfinished text. Return early in handleWindowKeyDown when isComposing is set or keyCode is 229 (Safari fires the confirming keydown after compositionend). Fixes #671 --- .changeset/hotkeys-ime-composition.md | 5 +++++ .../hooks/_internal/useSingletonHotkeys.tsx | 3 +++ .../src/hooks/tests/useHotkeys.stories.tsx | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 .changeset/hotkeys-ime-composition.md diff --git a/.changeset/hotkeys-ime-composition.md b/.changeset/hotkeys-ime-composition.md new file mode 100644 index 00000000..a8145a4c --- /dev/null +++ b/.changeset/hotkeys-ime-composition.md @@ -0,0 +1,5 @@ +--- +"reshaped": patch +--- + +Fixed `useHotkeys` reacting to keys that belong to an IME composition. With a Japanese, Chinese or Korean input method, the Enter that confirms a conversion no longer triggers hotkeys, so `Autocomplete` no longer selects the highlighted option or calls `onEnter` with unfinished text. diff --git a/packages/reshaped/src/hooks/_internal/useSingletonHotkeys.tsx b/packages/reshaped/src/hooks/_internal/useSingletonHotkeys.tsx index 0943687f..07ce4955 100644 --- a/packages/reshaped/src/hooks/_internal/useSingletonHotkeys.tsx +++ b/packages/reshaped/src/hooks/_internal/useSingletonHotkeys.tsx @@ -250,6 +250,9 @@ const HotkeysProvider: React.FC<{ children: React.ReactNode }> = (props) => { (e: KeyboardEvent) => { // Browsers trigger keyboard event without passing e.key when you click on autocomplete if (!e.key) return; + // Keys handled by an IME are not hotkeys. Safari fires the Enter that confirms + // a composition after compositionend, so only keyCode 229 marks it + if (e.isComposing || e.keyCode === 229) return; const nextPressedMap = addPressedKey(e) ?? pressedMapRef.current; globalHotkeyStore.handleKeyDown(nextPressedMap, e); diff --git a/packages/reshaped/src/hooks/tests/useHotkeys.stories.tsx b/packages/reshaped/src/hooks/tests/useHotkeys.stories.tsx index 9fcff815..e8b522cb 100644 --- a/packages/reshaped/src/hooks/tests/useHotkeys.stories.tsx +++ b/packages/reshaped/src/hooks/tests/useHotkeys.stories.tsx @@ -211,3 +211,22 @@ export const optionModified: StoryObj<{ expect(args.handleHotkey).toHaveBeenCalledTimes(1); }, }; + +export const imeComposition: StoryObj<{ handleHotkey: ReturnType }> = { + name: "ignores IME composition", + args: { + handleHotkey: fn(), + }, + render: (args) => , + play: async ({ args }) => { + window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", isComposing: true })); + window.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" })); + // Safari ends the composition before keydown, so only keyCode 229 marks it + window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", keyCode: 229 })); + window.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" })); + expect(args.handleHotkey).not.toHaveBeenCalled(); + + await userEvent.keyboard("{Enter}"); + expect(args.handleHotkey).toHaveBeenCalledTimes(1); + }, +};