diff --git a/apps/app/src/components/commands/CommandPalette.test.tsx b/apps/app/src/components/commands/CommandPalette.test.tsx index 7d546c9711..45d96e6ee6 100644 --- a/apps/app/src/components/commands/CommandPalette.test.tsx +++ b/apps/app/src/components/commands/CommandPalette.test.tsx @@ -303,6 +303,53 @@ describe("CommandPalette", () => { expect(selectedOption()?.textContent).toBe(titles[0]); }); + it.each(["Enter", "ArrowDown", "ArrowUp", "Home", "End"])( + "leaves %s to an active IME composition", + async (key) => { + renderPalette(); + openPalette(); + await waitFor(() => expect(searchField()).toBeTruthy()); + fireEvent.keyDown(searchField(), { key: "ArrowDown" }); + fireEvent.keyDown(searchField(), { key: "ArrowDown" }); + const activeDescendant = searchField().getAttribute( + "aria-activedescendant", + ); + + fireEvent.compositionStart(searchField()); + const composingKey = new KeyboardEvent("keydown", { + key, + isComposing: true, + bubbles: true, + cancelable: true, + }); + fireEvent(searchField(), composingKey); + + expect(composingKey.defaultPrevented).toBe(false); + + expect(screen.getByRole("combobox")).toBeTruthy(); + expect(searchField().getAttribute("aria-activedescendant")).toBe( + activeDescendant, + ); + expect(testState.calls).toEqual([]); + + fireEvent.compositionEnd(searchField()); + if (key !== "Enter") { + const navigation = new KeyboardEvent("keydown", { + key, + bubbles: true, + cancelable: true, + }); + fireEvent(searchField(), navigation); + + expect(navigation.defaultPrevented).toBe(true); + expect(searchField().getAttribute("aria-activedescendant")).not.toBe( + activeDescendant, + ); + expect(testState.calls).toEqual([]); + } + }, + ); + it("runs the highlighted command, closes, and restores focus", async () => { renderPalette(); openPalette(); @@ -319,6 +366,42 @@ describe("CommandPalette", () => { expect(document.activeElement).toBe(screen.getByTestId("origin")); }); + it("keeps composition confirmation separate from command activation", async () => { + renderPalette(); + openPalette(); + await waitFor(() => expect(searchField()).toBeTruthy()); + + fireEvent.change(searchField(), { target: { value: ">toggle panel" } }); + await waitFor(() => + expect(selectedOption()?.textContent).toContain("Toggle panel"), + ); + const input = searchField(); + fireEvent.compositionStart(input); + const confirmation = new KeyboardEvent("keydown", { + key: "Enter", + isComposing: true, + bubbles: true, + cancelable: true, + }); + fireEvent(input, confirmation); + + expect(confirmation.defaultPrevented).toBe(false); + expect(screen.queryByRole("combobox")).toBe(input); + expect(testState.calls).toEqual([]); + + fireEvent.compositionEnd(input); + const activation = new KeyboardEvent("keydown", { + key: "Enter", + bubbles: true, + cancelable: true, + }); + fireEvent(input, activation); + + expect(activation.defaultPrevented).toBe(true); + await waitFor(() => expect(testState.calls).toEqual(["panel.toggle"])); + expect(screen.queryByRole("combobox")).toBeNull(); + }); + it("runs a compact selection once after restoring focus", async () => { renderPalette(true); openPalette(); diff --git a/apps/app/src/components/commands/CommandPalette.tsx b/apps/app/src/components/commands/CommandPalette.tsx index b056aba9ac..13cc7d2321 100644 --- a/apps/app/src/components/commands/CommandPalette.tsx +++ b/apps/app/src/components/commands/CommandPalette.tsx @@ -248,6 +248,7 @@ export function CommandPalette({ threadId, projectId }: CommandPaletteProps) { const handleKeyDown = useCallback( (event: ReactKeyboardEvent) => { + if (event.nativeEvent.isComposing) return; if (resultCount === 0) return; if (event.key === "ArrowDown") { event.preventDefault();