From af25de000fd029ee4005be3ffac01dba7ae55e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=8F=9C=20Cai?= Date: Wed, 2 Sep 2026 20:22:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(Select):=20=E4=BF=AE=E5=A4=8D=20filterable?= =?UTF-8?q?=20=E4=B8=8B=E4=B8=AD=E6=96=87=E8=BE=93=E5=85=A5=E6=B3=95?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E9=80=89=E4=B8=AD=E5=90=8E=E5=86=8D=E6=AC=A1?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E4=BC=9A=E6=AE=8B=E7=95=99=E4=B8=8A=E6=AC=A1?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E5=86=85=E5=AE=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Input 在合成输入(composition)期间使用内部状态 composingValue 渲染,外部 value 被程序清空时该状态不会同步。Select 选中选项后会清空筛选词,但 composingValue 仍保留上一次的合成内容,导致再次聚焦进入合成态时,输入框回显旧内容并在其后累加新输入。 - fix(Input): 合成输入开始时以输入框当前内容为起点重置 composingValue - test: 补充 Input 受控清空后再次合成输入、Select filterable 中文输入法筛选的回归测试 closes #4382 --- packages/components/input/Input.tsx | 3 ++ .../components/input/__tests__/input.test.tsx | 36 +++++++++++++ .../select/__tests__/select.test.tsx | 51 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/packages/components/input/Input.tsx b/packages/components/input/Input.tsx index f9ed726335..b3fd4238f1 100644 --- a/packages/components/input/Input.tsx +++ b/packages/components/input/Input.tsx @@ -348,6 +348,9 @@ const Input = forwardRefWithStatics( const { currentTarget: { value }, } = e; + // 合成期间输入框展示的是 composingValue,而外部 value 可能已被程序清空(如 Select 选中选项后清空筛选词), + // 此时需要以输入框当前内容为起点重新累计,否则上一次的合成内容会被残留并累加 + setComposingValue(value); onCompositionstart?.(value, { e }); } function handleCompositionEnd(e: React.CompositionEvent) { diff --git a/packages/components/input/__tests__/input.test.tsx b/packages/components/input/__tests__/input.test.tsx index acf778a463..e8c36fe967 100644 --- a/packages/components/input/__tests__/input.test.tsx +++ b/packages/components/input/__tests__/input.test.tsx @@ -79,6 +79,42 @@ describe('Input 组件测试', () => { expect(onCompositionEndFn).toHaveBeenCalled(); expect(InputDom.value).toBe([InputValue, InputValue, InputValue].join('')); }); + test('composing value should not be kept when value is reset by outside', async () => { + // 模拟中文输入法的一次合成输入 + const imeInput = (input: HTMLInputElement, value: string) => { + fireEvent.compositionStart(input, { target: { value: input.value } }); + fireEvent.change(input, { target: { value } }); + fireEvent.compositionEnd(input, { target: { value } }); + }; + + const ControlledInput = () => { + const [value, setValue] = React.useState(''); + return ( + <> + setValue(v as string)} /> + + + ); + }; + const { queryByPlaceholderText, getByText } = render(); + const InputDom = queryByPlaceholderText(InputPlaceholder) as HTMLInputElement; + + imeInput(InputDom, '苹'); + expect(InputDom.value).toBe('苹'); + + // 外部清空输入框内容后,再次进入合成态不应残留上一次的输入 + fireEvent.click(getByText('reset')); + expect(InputDom.value).toBe(''); + + fireEvent.compositionStart(InputDom, { target: { value: InputDom.value } }); + expect(InputDom.value).toBe(''); + + fireEvent.change(InputDom, { target: { value: 'xiang' } }); + fireEvent.compositionEnd(InputDom, { target: { value: '香' } }); + expect(InputDom.value).toBe('香'); + }); test('keyDown', async () => { const user = userEvent.setup(); const onEnterFn = vi.fn(); diff --git a/packages/components/select/__tests__/select.test.tsx b/packages/components/select/__tests__/select.test.tsx index 32dc07e70e..8022b9dfdd 100644 --- a/packages/components/select/__tests__/select.test.tsx +++ b/packages/components/select/__tests__/select.test.tsx @@ -359,6 +359,57 @@ describe('Select 组件测试', () => { expect(document.querySelector(popupSelector)).toHaveTextContent('无数据'); }); + test('可过滤选择器中文输入法测试', async () => { + const testId = 'test-id-ime'; + // 模拟中文输入法的一次合成输入 + const imeInput = (input: HTMLInputElement, value: string) => { + fireEvent.compositionStart(input, { target: { value: input.value } }); + fireEvent.change(input, { target: { value } }); + fireEvent.compositionEnd(input, { target: { value } }); + }; + const cnOptions = [ + { label: '苹果', value: 'apple' }, + { label: '香蕉', value: 'banana' }, + { label: '橙子', value: 'orange' }, + ]; + + const FilterableSelect = () => { + const [value, setValue] = useState(); + const onChange = (value) => { + setValue(value); + }; + + return ( + + ); + }; + const { getByPlaceholderText, getByText } = render(); + const input = getByPlaceholderText(testId) as HTMLInputElement; + + // 中文输入法输入“苹”,筛选出“苹果” + fireEvent.click(input); + imeInput(input, '苹'); + expect(input).toHaveValue('苹'); + expect(document.querySelector(popupSelector)).toHaveTextContent('苹果'); + + // 选中“苹果”后,输入框展示选中项 + fireEvent.click(getByText('苹果')); + expect(input).toHaveValue('苹果'); + + // 再次聚焦并输入,不应保留上一次的筛选内容 + fireEvent.click(input); + expect(input).toHaveValue(''); + fireEvent.compositionStart(input, { target: { value: input.value } }); + expect(input).toHaveValue(''); + fireEvent.change(input, { target: { value: 'xiang' } }); + fireEvent.compositionEnd(input, { target: { value: '香' } }); + expect(input).toHaveValue('香'); + }); + test('远程搜索测试', async () => { const user = userEvent.setup(); render();