From 5d985a6563adf15cff068ebc5afb6ccbb6a18dec Mon Sep 17 00:00:00 2001 From: keiju-fujiwara <77735745+fffk10@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:17:24 +0900 Subject: [PATCH] fix: suppress Enter submit during IME composition in workflow creation popup (#804) * fix: guard Enter submit against IME composition in workflow creation popup Pressing Enter during Japanese IME conversion was triggering workflow submission instead of only confirming the candidate. Extract shouldSuppressEnterSubmit() and check event.isComposing before submit in NewWorkspaceComposerModal, NewWorkspacePage, and the task search handler. Fixes #742. * fix: use nativeEvent.isComposing for React SyntheticEvent in task search handler * refactor: move new-workspace-enter-guard to src/renderer/src/lib --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> --- .../components/NewWorkspaceComposerModal.tsx | 3 +- .../src/components/NewWorkspacePage.tsx | 10 ++++++ .../src/lib/new-workspace-enter-guard.test.ts | 33 +++++++++++++++++++ .../src/lib/new-workspace-enter-guard.ts | 19 +++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/lib/new-workspace-enter-guard.test.ts create mode 100644 src/renderer/src/lib/new-workspace-enter-guard.ts diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index ea9b4e3f681..30c5230b505 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -4,6 +4,7 @@ import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/compone import NewWorkspaceComposerCard from '@/components/NewWorkspaceComposerCard' import { useComposerState } from '@/hooks/useComposerState' import type { LinkedWorkItemSummary } from '@/lib/new-workspace' +import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard' type ComposerModalData = { prefilledName?: string @@ -102,7 +103,7 @@ function ComposerModalBody({ if (createDisabled) { return } - if (target instanceof HTMLTextAreaElement && event.shiftKey) { + if (shouldSuppressEnterSubmit(event, target instanceof HTMLTextAreaElement)) { return } event.preventDefault() diff --git a/src/renderer/src/components/NewWorkspacePage.tsx b/src/renderer/src/components/NewWorkspacePage.tsx index 5dd155f91aa..848b1b0d807 100644 --- a/src/renderer/src/components/NewWorkspacePage.tsx +++ b/src/renderer/src/components/NewWorkspacePage.tsx @@ -34,6 +34,7 @@ import { getLinkedWorkItemSuggestedName, getTaskPresetQuery } from '@/lib/new-wo import type { LinkedWorkItemSummary } from '@/lib/new-workspace' import { isGitRepoKind } from '../../../shared/repo-kind' import type { GitHubWorkItem, TaskViewPresetId } from '../../../shared/types' +import { shouldSuppressEnterSubmit } from '@/lib/new-workspace-enter-guard' type TaskSource = 'github' | 'linear' type TaskQueryPreset = { @@ -332,6 +333,15 @@ export default function NewWorkspacePage(): React.JSX.Element { const handleTaskSearchKeyDown = useCallback( (event: React.KeyboardEvent): void => { if (event.key === 'Enter') { + // React SyntheticEvent does not expose isComposing; use nativeEvent. + if ( + shouldSuppressEnterSubmit( + { isComposing: event.nativeEvent.isComposing, shiftKey: event.shiftKey }, + false + ) + ) { + return + } event.preventDefault() handleApplyTaskSearch() } diff --git a/src/renderer/src/lib/new-workspace-enter-guard.test.ts b/src/renderer/src/lib/new-workspace-enter-guard.test.ts new file mode 100644 index 00000000000..cfed351a043 --- /dev/null +++ b/src/renderer/src/lib/new-workspace-enter-guard.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest' +import { shouldSuppressEnterSubmit } from './new-workspace-enter-guard' + +function makeEvent(overrides: Partial<{ isComposing: boolean; shiftKey: boolean }>): { + isComposing: boolean + shiftKey: boolean +} { + return { isComposing: false, shiftKey: false, ...overrides } +} + +describe('shouldSuppressEnterSubmit', () => { + it('returns false for a plain Enter with no composition', () => { + expect(shouldSuppressEnterSubmit(makeEvent({}), false)).toBe(false) + }) + + it('returns true when IME composition is active', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ isComposing: true }), false)).toBe(true) + }) + + it('returns true for Shift+Enter inside a textarea', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ shiftKey: true }), true)).toBe(true) + }) + + it('returns false for Shift+Enter inside a non-textarea element', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ shiftKey: true }), false)).toBe(false) + }) + + it('returns true when both isComposing and shiftKey are true (textarea)', () => { + expect(shouldSuppressEnterSubmit(makeEvent({ isComposing: true, shiftKey: true }), true)).toBe( + true + ) + }) +}) diff --git a/src/renderer/src/lib/new-workspace-enter-guard.ts b/src/renderer/src/lib/new-workspace-enter-guard.ts new file mode 100644 index 00000000000..69523c01386 --- /dev/null +++ b/src/renderer/src/lib/new-workspace-enter-guard.ts @@ -0,0 +1,19 @@ +/** + * Returns true when an Enter keydown event should be suppressed for submit actions. + * + * Two cases must be blocked: + * 1. IME composition is active — Enter only confirms the conversion candidate. + * 2. Shift+Enter inside a textarea — intended as a newline, not a submit. + */ +export function shouldSuppressEnterSubmit( + event: { isComposing: boolean; shiftKey: boolean }, + isTextarea: boolean +): boolean { + if (event.isComposing) { + return true + } + if (isTextarea && event.shiftKey) { + return true + } + return false +}