diff --git a/src/components/AiDock.tsx b/src/components/AiDock.tsx index 91cfcf00b..414c9a3ce 100644 --- a/src/components/AiDock.tsx +++ b/src/components/AiDock.tsx @@ -45,6 +45,7 @@ import { CodeBlock } from '~/components/markdown/CodeBlock' import { copyTextToClipboard } from '~/utils/browser-effects' import { InlineCode } from '~/ui/InlineCode' import { isSafeHref } from '~/utils/url-boundary' +import { submitOnEnter } from '~/utils/ai-dock-enter' import { type AlgoliaHit, dedupeSearchHitsByPage, @@ -1988,12 +1989,7 @@ function InputBar({ setSearchQuery(nextQuery) refine(nextQuery) }} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault() - submitQuestion() - } - }} + onKeyDown={(event) => submitOnEnter(event, submitQuestion)} className={twMerge( 'w-full outline-none [&::-webkit-search-cancel-button]:hidden bg-transparent text-gray-800 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500', isDock ? 'text-[13px]' : 'text-sm', diff --git a/src/utils/ai-dock-enter.ts b/src/utils/ai-dock-enter.ts new file mode 100644 index 000000000..cfc6c991c --- /dev/null +++ b/src/utils/ai-dock-enter.ts @@ -0,0 +1,19 @@ +type EnterKeyEvent = { + key: string + nativeEvent: Pick + preventDefault: () => void +} + +export function submitOnEnter(event: EnterKeyEvent, submit: () => void) { + if ( + event.key !== 'Enter' || + event.nativeEvent.isComposing || + // Safari can fire compositionend before this keydown, so isComposing is + // already false. keyCode is deprecated, but it is still 229 here. + event.nativeEvent.keyCode === 229 + ) { + return + } + event.preventDefault() + submit() +} diff --git a/tests/ai-dock-enter.test.ts b/tests/ai-dock-enter.test.ts new file mode 100644 index 000000000..7a394ebaf --- /dev/null +++ b/tests/ai-dock-enter.test.ts @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { submitOnEnter } from '../src/utils/ai-dock-enter' + +function press(key: string, isComposing: boolean, keyCode: number) { + let prevented = 0 + let submitted = 0 + submitOnEnter( + { + key, + nativeEvent: { isComposing, keyCode }, + preventDefault: () => { + prevented++ + }, + }, + () => { + submitted++ + }, + ) + return { prevented, submitted } +} + +test('Enter during composition neither submits nor prevents default', () => { + assert.deepEqual(press('Enter', true, 13), { prevented: 0, submitted: 0 }) +}) + +test('Enter with isComposing=false and keyCode=229 neither submits nor prevents default', () => { + assert.deepEqual(press('Enter', false, 229), { prevented: 0, submitted: 0 }) +}) + +test('ordinary Enter submits once', () => { + assert.deepEqual(press('Enter', false, 13), { prevented: 1, submitted: 1 }) +}) + +test('non-Enter key does not submit', () => { + assert.deepEqual(press('a', false, 65), { prevented: 0, submitted: 0 }) +})