fix: add IME-safe onPressEnter prop to base Input component (#31757)

The base Input component lacked IME composition detection, causing
Enter key presses during CJK input method candidate selection to
mistakenly trigger form submissions.

Add an `onPressEnter` prop with built-in IME safety using
compositionStart/End tracking and nativeEvent.isComposing checks
(with Safari 50ms delay workaround). Migrate all 5 call sites
from manual onKeyDown Enter detection to onPressEnter.
This commit is contained in:
yyh 2026-01-30 17:56:25 +08:00
parent f90fa2b186
commit 3997749867
No known key found for this signature in database
7 changed files with 44 additions and 41 deletions

View File

@ -145,10 +145,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
value={password}
onChange={e => setPassword(e.target.value)}
id="password"
onKeyDown={(e) => {
if (e.key === 'Enter')
handleEmailPasswordLogin()
}}
onPressEnter={() => handleEmailPasswordLogin()}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}

View File

@ -1,5 +1,5 @@
import type { VariantProps } from 'class-variance-authority'
import type { ChangeEventHandler, CSSProperties, FocusEventHandler } from 'react'
import type { ChangeEventHandler, CSSProperties, FocusEventHandler, KeyboardEventHandler } from 'react'
import { RiCloseCircleFill, RiErrorWarningLine, RiSearchLine } from '@remixicon/react'
import { cva } from 'class-variance-authority'
import { noop } from 'es-toolkit/function'
@ -33,6 +33,7 @@ export type InputProps = {
wrapperClassName?: string
styleCss?: CSSProperties
unit?: string
onPressEnter?: KeyboardEventHandler<HTMLInputElement>
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> & VariantProps<typeof inputVariants>
const removeLeadingZeros = (value: string) => value.replace(/^(-?)0+(?=\d)/, '$1')
@ -52,10 +53,26 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
placeholder,
onChange = noop,
onBlur = noop,
onKeyDown,
onPressEnter,
unit,
...props
}, ref) => {
const { t } = useTranslation()
const isComposingRef = React.useRef(false)
const handleCompositionStart = () => {
isComposingRef.current = true
}
const handleCompositionEnd = () => {
setTimeout(() => {
isComposingRef.current = false
}, 50)
}
const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = (e) => {
if (onPressEnter && e.key === 'Enter' && !e.nativeEvent.isComposing && !isComposingRef.current)
onPressEnter(e)
onKeyDown?.(e)
}
const handleNumberChange: ChangeEventHandler<HTMLInputElement> = (e) => {
if (value === 0) {
// remove leading zeros
@ -108,6 +125,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
onBlur={props.type === 'number' ? handleNumberBlur : onBlur}
disabled={disabled}
{...props}
onKeyDown={handleKeyDown}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
/>
{!!(showClearIcon && value && !disabled && !destructive) && (
<div

View File

@ -71,12 +71,12 @@ const CustomizedPagination: FC<Props> = ({
setShowInput(false)
}
const handleInputPressEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault()
handleInputConfirm()
}
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
handleInputConfirm()
}
else if (e.key === 'Escape') {
if (e.key === 'Escape') {
e.preventDefault()
setInputValue(current + 1)
setShowInput(false)
@ -132,6 +132,7 @@ const CustomizedPagination: FC<Props> = ({
autoFocus
value={inputValue}
onChange={handleInputChange}
onPressEnter={handleInputPressEnter}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
/>

View File

@ -319,22 +319,17 @@ const GotoAnything: FC<Props> = ({
if (!e.target.value.startsWith('@') && !e.target.value.startsWith('/'))
clearSelection()
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const query = searchQuery.trim()
// Check if it's a complete slash command
if (query.startsWith('/')) {
const commandName = query.substring(1).split(' ')[0]
const handler = slashCommandRegistry.findCommand(commandName)
// If it's a direct mode command, execute immediately
const isAvailable = handler?.isAvailable?.() ?? true
if (handler?.mode === 'direct' && handler.execute && isAvailable) {
e.preventDefault()
handler.execute()
setShow(false)
setSearchQuery('')
}
onPressEnter={(e) => {
const query = searchQuery.trim()
if (query.startsWith('/')) {
const commandName = query.substring(1).split(' ')[0]
const handler = slashCommandRegistry.findCommand(commandName)
const isAvailable = handler?.isAvailable?.() ?? true
if (handler?.mode === 'direct' && handler.execute && isAvailable) {
e.preventDefault()
handler.execute()
setShow(false)
setSearchQuery('')
}
}
}}

View File

@ -139,10 +139,7 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
id="password"
value={password}
onChange={e => setPassword(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter')
handleEmailPasswordLogin()
}}
onPressEnter={() => handleEmailPasswordLogin()}
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
placeholder={t('passwordPlaceholder', { ns: 'login' }) || ''}

View File

@ -103,12 +103,10 @@ export default function InviteSettingsPage() {
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('namePlaceholder', { ns: 'login' }) || ''}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
handleActivate()
}
onPressEnter={(e) => {
e.preventDefault()
e.stopPropagation()
handleActivate()
}}
/>
</div>

View File

@ -1234,11 +1234,6 @@
"count": 1
}
},
"app/components/base/input/index.tsx": {
"react-refresh/only-export-components": {
"count": 1
}
},
"app/components/base/logo/dify-logo.tsx": {
"react-refresh/only-export-components": {
"count": 2