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>
This commit is contained in:
keiju-fujiwara
2026-04-18 21:17:24 -07:00
committed by GitHub
co-authored by Neil
parent 159081c2e6
commit 5d985a6563
4 changed files with 64 additions and 1 deletions
@@ -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()
@@ -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<HTMLInputElement>): 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()
}
@@ -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
)
})
})
@@ -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
}