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); + }, +};