fix(web): handle IME composition in DelimiterInput (#34660)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
plind 2026-04-13 19:49:37 -07:00 committed by GitHub
parent fbcab757d5
commit f8b249e649
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 41 additions and 7 deletions

View File

@ -33,6 +33,22 @@ describe('DelimiterInput', () => {
// Tooltip triggers render; component mounts without error
expect(screen.getByText(`${ns}.stepTwo.separator`)).toBeInTheDocument()
})
it('should suppress onChange during IME composition', () => {
const onChange = vi.fn()
const finalValue = 'wu'
render(<DelimiterInput value="" onChange={onChange} />)
const input = screen.getByPlaceholderText(`${ns}.stepTwo.separatorPlaceholder`)
fireEvent.compositionStart(input)
fireEvent.change(input, { target: { value: 'w' } })
fireEvent.change(input, { target: { value: finalValue } })
expect(onChange).not.toHaveBeenCalled()
fireEvent.compositionEnd(input)
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange.mock.calls[0][0].target.value).toBe(finalValue)
})
})
describe('MaxLengthInput', () => {

View File

@ -1,6 +1,7 @@
import type { FC, PropsWithChildren, ReactNode } from 'react'
import type { InputProps } from '@/app/components/base/input'
import type { NumberFieldInputProps, NumberFieldRootProps, NumberFieldSize } from '@/app/components/base/ui/number-field'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Tooltip from '@/app/components/base/tooltip'
@ -16,7 +17,7 @@ import {
import { env } from '@/env'
const TextLabel: FC<PropsWithChildren> = (props) => {
return <label className="text-xs font-semibold leading-none text-text-secondary">{props.children}</label>
return <label className="text-xs leading-none font-semibold text-text-secondary">{props.children}</label>
}
const FormField: FC<PropsWithChildren<{ label: ReactNode }>> = (props) => {
@ -28,8 +29,11 @@ const FormField: FC<PropsWithChildren<{ label: ReactNode }>> = (props) => {
)
}
export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) => {
export const DelimiterInput: FC<InputProps & { tooltip?: string }> = ({ tooltip, onChange, value, ...rest }) => {
const { t } = useTranslation()
const isComposing = useRef(false)
const [compositionValue, setCompositionValue] = useState('')
return (
<FormField label={(
<div className="mb-1 flex items-center">
@ -37,7 +41,7 @@ export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) =>
<Tooltip
popupContent={(
<div className="max-w-[200px]">
{props.tooltip || t('stepTwo.separatorTip', { ns: 'datasetCreation' })}
{tooltip || t('stepTwo.separatorTip', { ns: 'datasetCreation' })}
</div>
)}
/>
@ -48,7 +52,24 @@ export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) =>
type="text"
className="h-9"
placeholder={t('stepTwo.separatorPlaceholder', { ns: 'datasetCreation' })!}
{...props}
value={isComposing.current ? compositionValue : value}
onChange={(e) => {
if (isComposing.current)
setCompositionValue(e.target.value)
else
onChange?.(e)
}}
onCompositionStart={() => {
isComposing.current = true
setCompositionValue(String(value ?? ''))
}}
onCompositionEnd={(e) => {
const committed = e.currentTarget.value
isComposing.current = false
setCompositionValue('')
onChange?.({ ...e, target: { ...e.target, value: committed } } as unknown as React.ChangeEvent<HTMLInputElement>)
}}
{...rest}
/>
</FormField>
)

View File

@ -4291,9 +4291,6 @@
"app/components/datasets/create/step-two/components/inputs.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1
}
},
"app/components/datasets/create/step-two/components/option-card.tsx": {