Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { ComposerKeyDownEvent, shouldSubmitOnEnter } from "../_shared/utils/composerKeyboard";

const keyDown = (overrides: Partial<ComposerKeyDownEvent> = {}): ComposerKeyDownEvent => ({
key: "Enter",
shiftKey: false,
keyCode: 13,
nativeEvent: { isComposing: false },
...overrides,
});

describe("shouldSubmitOnEnter", () => {
it("submits on a plain Enter", () => {
expect(shouldSubmitOnEnter(keyDown())).toBe(true);
});

it("does not submit on Shift+Enter — that inserts a newline", () => {
expect(shouldSubmitOnEnter(keyDown({ shiftKey: true }))).toBe(false);
});

it("ignores keys other than Enter", () => {
expect(shouldSubmitOnEnter(keyDown({ key: "a", keyCode: 65 }))).toBe(false);
});

it("does not submit while an IME composition is open (isComposing)", () => {
expect(shouldSubmitOnEnter(keyDown({ nativeEvent: { isComposing: true } }))).toBe(false);
});

it("does not submit when the browser reports the IME sentinel keyCode 229", () => {
// Safari and older Chromium leave isComposing unset on this keydown.
expect(shouldSubmitOnEnter(keyDown({ keyCode: 229 }))).toBe(false);
});

it("submits on the Enter that follows a finished composition", () => {
expect(shouldSubmitOnEnter(keyDown({ nativeEvent: { isComposing: false } }))).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* The subset of a `keydown` event the composers need to decide whether `Enter`
* submits. React's `KeyboardEvent<HTMLTextAreaElement>` satisfies it
* structurally, so call sites pass the synthetic event straight through and
* tests can build a plain object.
*/
export interface ComposerKeyDownEvent {
key: string;
shiftKey: boolean;
keyCode: number;
nativeEvent: { isComposing: boolean };
}

/**
* Safari and older Chromium report `keyCode` 229 for a keydown consumed by an
* open IME composition, and do not always have `isComposing` set on that same
* event. Checking both covers each browser's timing.
*/
const IME_KEY_CODE = 229;

/**
* Whether an `Enter` keydown in a composer textarea should send the draft.
*
* `Enter` sends and `Shift+Enter` inserts a newline — except while an IME
* composition is open, where `Enter` belongs to the IME (it commits the
* conversion candidate) and must not reach the composer.
*
* Without this guard, Windows Voice Typing holds a composition session open
* across dictation: a physical `Enter` sends and clears the textarea, the
* composition then finalizes and fires one more `onChange`, and the dictated
* text reappears in the box the user just emptied. Every CJK IME hits the same
* path and sends a half-converted phrase instead of committing it.
*
* Consequence, shared with every IME-aware composer: the `Enter` that closes a
* composition does not send. The next one does.
*/
export const shouldSubmitOnEnter = (event: ComposerKeyDownEvent): boolean => {
if (event.key !== "Enter" || event.shiftKey) return false;
return !event.nativeEvent.isComposing && event.keyCode !== IME_KEY_CODE;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useLayoutContext } from "../../../context/LayoutContext";
import { useAutoFocus } from "../../../hooks/useAutoFocus";
import { useComposerState } from "../../../hooks/useComposerState";
import { IconButton } from "../../IconButton";
import { shouldSubmitOnEnter } from "../_shared/utils/composerKeyboard";

export interface ComposerProps {
className?: string;
Expand Down Expand Up @@ -87,7 +88,7 @@ export const Composer = ({ className, placeholder = "Type your query here" }: Co
placeholder={placeholder}
rows={1}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
if (shouldSubmitOnEnter(e)) {
e.preventDefault();
handleSubmit();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useLayoutContext } from "../../../context/LayoutContext";
import { useAutoFocus } from "../../../hooks/useAutoFocus";
import { useComposerState } from "../../../hooks/useComposerState";
import { IconButton } from "../../IconButton";
import { shouldSubmitOnEnter } from "../_shared/utils/composerKeyboard";

export interface DesktopWelcomeComposerProps {
className?: string;
Expand Down Expand Up @@ -88,7 +89,7 @@ export const DesktopWelcomeComposer = ({
placeholder={placeholder}
rows={1}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
if (shouldSubmitOnEnter(e)) {
e.preventDefault();
handleSubmit();
}
Expand Down