diff --git a/src/PickerInput/Selector/Input.tsx b/src/PickerInput/Selector/Input.tsx index 22051545c..fe9660f6d 100644 --- a/src/PickerInput/Selector/Input.tsx +++ b/src/PickerInput/Selector/Input.tsx @@ -142,10 +142,18 @@ const Input = React.forwardRef((props, ref) => { // Directly trigger `onChange` if `format` is empty const onInternalChange: React.ChangeEventHandler = (event) => { + const text = event.target.value; + + // Handle manual clear only when clearIcon is present (allowClear was enabled) + // If clearIcon is not set, reset back to previous valid date instead + if (text === '' && value !== '' && clearIcon) { + onChange(''); + setInputValue(''); + return; + } + // Hack `onChange` with format to do nothing if (!format) { - const text = event.target.value; - onModify(text); setInputValue(text); onChange(text); @@ -267,7 +275,6 @@ const Input = React.forwardRef((props, ref) => { nextCellText = ''; nextFillText = cellFormat; break; - // =============== Arrows =============== // Left key case 'ArrowLeft': diff --git a/src/PickerInput/Selector/SingleSelector/index.tsx b/src/PickerInput/Selector/SingleSelector/index.tsx index 7a373b9c0..f5df0f8c5 100644 --- a/src/PickerInput/Selector/SingleSelector/index.tsx +++ b/src/PickerInput/Selector/SingleSelector/index.tsx @@ -130,8 +130,14 @@ function SingleSelector( const rootProps = useRootProps(restProps); // ======================== Change ======================== - const onSingleChange = (date: DateType) => { - onChange([date]); + const onSingleChange = (date: DateType | null) => { + if (date === null && clearIcon) { + // Only allow manual clear when clearIcon is present (allowClear was enabled) + onClear?.(); + } else if (date !== null) { + onChange([date]); + } + // If date is null but clearIcon is not set, do nothing - let it reset to previous value }; const onMultipleRemove = (date: DateType) => { diff --git a/src/PickerInput/Selector/hooks/useInputProps.ts b/src/PickerInput/Selector/hooks/useInputProps.ts index 251777982..c9156df93 100644 --- a/src/PickerInput/Selector/hooks/useInputProps.ts +++ b/src/PickerInput/Selector/hooks/useInputProps.ts @@ -185,8 +185,14 @@ export default function useInputProps( return; } + // Handle intentional clearing: when text is empty, trigger onChange with null + if (text === '') { + onInvalid(false, index); // Reset invalid state before clearing the value + onChange(null, index); + return; + } + // Tell outer that the value typed is invalid. - // If text is empty, it means valid. onInvalid(!!text, index); }, onHelp: () => { diff --git a/tests/manual-clear.spec.tsx b/tests/manual-clear.spec.tsx new file mode 100644 index 000000000..c113dacba --- /dev/null +++ b/tests/manual-clear.spec.tsx @@ -0,0 +1,304 @@ +import { fireEvent, render } from '@testing-library/react'; +import React from 'react'; +import { Picker, RangePicker } from '../src'; +import dayGenerateConfig from '../src/generate/dayjs'; +import enUS from '../src/locale/en_US'; +import { getDay, openPicker, waitFakeTimer } from './util/commonUtil'; + +describe('Picker.ManualClear', () => { + beforeEach(() => { + jest.useFakeTimers().setSystemTime(getDay('1990-09-03 00:00:00').valueOf()); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + describe('Single Picker', () => { + it('should trigger onChange when manually clearing input', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const input = container.querySelector('input') as HTMLInputElement; + + openPicker(container); + fireEvent.change(input, { target: { value: '' } }); + + await waitFakeTimer(); + + expect(onChange).toHaveBeenCalledWith(null, null); + }); + + it('should NOT clear when allowClear is disabled - reset to previous value', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const input = container.querySelector('input') as HTMLInputElement; + + expect(input.value).toBe('2023-08-01'); + + openPicker(container); + fireEvent.change(input, { target: { value: '' } }); + fireEvent.blur(input); + + await waitFakeTimer(); + + expect(onChange).not.toHaveBeenCalled(); + expect(input.value).toBe('2023-08-01'); + }); + + it('should reset invalid partial input on blur without triggering onChange', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + const input = container.querySelector('input') as HTMLInputElement; + openPicker(container); + fireEvent.change(input, { target: { value: '2023-08' } }); + const initialOnChangeCallCount = onChange.mock.calls.length; + fireEvent.blur(input); + await waitFakeTimer(); + expect(onChange.mock.calls.length).toBe(initialOnChangeCallCount); + expect(input.value).toBe('2023-08-01'); + }); + + it('should work with different picker modes', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const input = container.querySelector('input') as HTMLInputElement; + + openPicker(container); + fireEvent.change(input, { target: { value: '' } }); + + await waitFakeTimer(); + + expect(onChange).toHaveBeenCalledWith(null, null); + }); + + it('should clear input value when manually clearing', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const input = container.querySelector('input') as HTMLInputElement; + + expect(input.value).toBe('2023-08-01'); + + openPicker(container); + fireEvent.change(input, { target: { value: '' } }); + + await waitFakeTimer(); + + expect(input.value).toBe(''); + }); + + it('should clear formatted input with mask format', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const input = container.querySelector('input') as HTMLInputElement; + + openPicker(container); + fireEvent.change(input, { target: { value: '' } }); + + await waitFakeTimer(); + + expect(onChange).toHaveBeenCalledWith(null, null); + expect(input.value).toBe(''); + }); + }); + + describe('Range Picker', () => { + it('should clear start input value when manually clearing', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const startInput = container.querySelectorAll('input')[0] as HTMLInputElement; + + openPicker(container, 0); + fireEvent.change(startInput, { target: { value: '' } }); + fireEvent.blur(startInput); + + await waitFakeTimer(); + + expect(startInput.value).toBe(''); + }); + + it('should clear end input value when manually clearing', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const endInput = container.querySelectorAll('input')[1] as HTMLInputElement; + + openPicker(container, 1); + fireEvent.change(endInput, { target: { value: '' } }); + fireEvent.blur(endInput); + + await waitFakeTimer(); + + expect(endInput.value).toBe(''); + }); + + it('should clear both input values when manually clearing', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const startInput = container.querySelectorAll('input')[0] as HTMLInputElement; + const endInput = container.querySelectorAll('input')[1] as HTMLInputElement; + + openPicker(container, 0); + fireEvent.change(startInput, { target: { value: '' } }); + fireEvent.blur(startInput); + await waitFakeTimer(); + + openPicker(container, 1); + fireEvent.change(endInput, { target: { value: '' } }); + fireEvent.blur(endInput); + await waitFakeTimer(); + + expect(startInput.value).toBe(''); + expect(endInput.value).toBe(''); + }); + + it('should clear input values when manually clearing', async () => { + const onChange = jest.fn(); + const { container } = render( + , + ); + + const startInput = container.querySelectorAll('input')[0] as HTMLInputElement; + + expect(startInput.value).toBe('2023-08-01'); + + openPicker(container, 0); + fireEvent.change(startInput, { target: { value: '' } }); + + await waitFakeTimer(); + + expect(startInput.value).toBe(''); + }); + }); + + describe('Comparison with clear button', () => { + it('manual clear should behave the same as clear button for Picker', async () => { + const onChangeManual = jest.fn(); + const onChangeClear = jest.fn(); + + const { container: container1 } = render( + , + ); + + const input1 = container1.querySelector('input') as HTMLInputElement; + openPicker(container1); + fireEvent.change(input1, { target: { value: '' } }); + await waitFakeTimer(); + + const { container: container2 } = render( + , + ); + + const clearBtn = container2.querySelector('.rc-picker-clear'); + fireEvent.mouseDown(clearBtn); + fireEvent.mouseUp(clearBtn); + fireEvent.click(clearBtn); + await waitFakeTimer(); + + expect(onChangeManual).toHaveBeenCalledWith(null, null); + expect(onChangeClear).toHaveBeenCalledWith(null, null); + }); + }); +});