diff --git a/.changeset/select-ime-enter-keycode-229.md b/.changeset/select-ime-enter-keycode-229.md new file mode 100644 index 000000000..150b22e42 --- /dev/null +++ b/.changeset/select-ime-enter-keycode-229.md @@ -0,0 +1,5 @@ +--- +"bits-ui": patch +--- + +fix(Select): don't commit the highlighted item on an IME composition-commit Enter in Safari (`keyCode === 229`), matching the guard already used in Command. This also fixes Combobox, which shares the same input keydown handler. diff --git a/packages/bits-ui/src/lib/bits/select/select.svelte.ts b/packages/bits-ui/src/lib/bits/select/select.svelte.ts index 508f904b4..02f79bb00 100644 --- a/packages/bits-ui/src/lib/bits/select/select.svelte.ts +++ b/packages/bits-ui/src/lib/bits/select/select.svelte.ts @@ -565,7 +565,10 @@ export class SelectInputState { return; } - if (e.key === kbd.ENTER && !e.isComposing) { + // `e.keyCode === 229` covers Safari + Japanese IME, where `isComposing` is not + // set while composing, so an IME-commit Enter would otherwise select an item. + // Mirrors the Enter guard in `command.svelte.ts`. + if (e.key === kbd.ENTER && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); const isCurrentSelectedValue = diff --git a/tests/src/tests/combobox/combobox.browser.test.ts b/tests/src/tests/combobox/combobox.browser.test.ts index 258b4d25e..9f87b822f 100644 --- a/tests/src/tests/combobox/combobox.browser.test.ts +++ b/tests/src/tests/combobox/combobox.browser.test.ts @@ -186,6 +186,31 @@ describe("combobox - single", () => { await expect.element(page.getByTestId("input")).toHaveValue("B"); }); + it("should not select an item on enter while composing with an IME (keyCode 229)", async () => { + await openSingle(); + await expect.element(page.getByTestId("input")).toHaveFocus(); + await userEvent.keyboard(kbd.ARROW_DOWN); + await expect.element(page.getByTestId("2")).toHaveAttribute("data-highlighted"); + + // Safari + Japanese IME deliver the composition-commit Enter with `keyCode === 229` + // and `isComposing === false`, so it must not commit the highlighted item. + page.getByTestId("input").element().dispatchEvent( + new KeyboardEvent("keydown", { + key: "Enter", + keyCode: 229, + bubbles: true, + cancelable: true, + }) + ); + await tick(); + await expect.element(page.getByTestId("content")).toBeVisible(); + await expect.element(page.getByTestId("input")).not.toHaveValue("B"); + + // a normal Enter (keyCode 13, not composing) still commits the selection + await userEvent.keyboard(kbd.ENTER); + await expect.element(page.getByTestId("input")).toHaveValue("B"); + }); + it("should render an input if the `name` prop is passed", async () => { const t = setupSingle(); await expect.element(t.getHiddenInput()).toBeInTheDocument();