From 3997749867df38df470ee55a64441af553499701 Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 30 Jan 2026 17:56:25 +0800 Subject: [PATCH] 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. --- .../components/mail-and-password-auth.tsx | 5 +--- web/app/components/base/input/index.tsx | 22 ++++++++++++++- web/app/components/base/pagination/index.tsx | 11 ++++---- web/app/components/goto-anything/index.tsx | 27 ++++++++----------- .../components/mail-and-password-auth.tsx | 5 +--- web/app/signin/invite-settings/page.tsx | 10 +++---- web/eslint-suppressions.json | 5 ---- 7 files changed, 44 insertions(+), 41 deletions(-) diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx index e49559401d..5739f58e90 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -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' }) || ''} diff --git a/web/app/components/base/input/index.tsx b/web/app/components/base/input/index.tsx index ae76b71a1c..034f57044c 100644 --- a/web/app/components/base/input/index.tsx +++ b/web/app/components/base/input/index.tsx @@ -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 } & Omit, 'size'> & VariantProps const removeLeadingZeros = (value: string) => value.replace(/^(-?)0+(?=\d)/, '$1') @@ -52,10 +53,26 @@ const Input = React.forwardRef(({ 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 = (e) => { + if (onPressEnter && e.key === 'Enter' && !e.nativeEvent.isComposing && !isComposingRef.current) + onPressEnter(e) + onKeyDown?.(e) + } const handleNumberChange: ChangeEventHandler = (e) => { if (value === 0) { // remove leading zeros @@ -108,6 +125,9 @@ const Input = React.forwardRef(({ onBlur={props.type === 'number' ? handleNumberBlur : onBlur} disabled={disabled} {...props} + onKeyDown={handleKeyDown} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} /> {!!(showClearIcon && value && !disabled && !destructive) && (
= ({ setShowInput(false) } + const handleInputPressEnter = (e: React.KeyboardEvent) => { + e.preventDefault() + handleInputConfirm() + } const handleInputKeyDown = (e: React.KeyboardEvent) => { - 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 = ({ autoFocus value={inputValue} onChange={handleInputChange} + onPressEnter={handleInputPressEnter} onKeyDown={handleInputKeyDown} onBlur={handleInputBlur} /> diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index 733e1d3162..5d404ddf7f 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -319,22 +319,17 @@ const GotoAnything: FC = ({ 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('') } } }} diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 92165bb65b..a4a25e7f9d 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -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' }) || ''} diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index c16a580b3a..8f9c05b8b6 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -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() }} />
diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index ae82d79919..7529197d68 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -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