Skip to content
13 changes: 10 additions & 3 deletions src/PickerInput/Selector/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,18 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {

// Directly trigger `onChange` if `format` is empty
const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (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);
Expand Down Expand Up @@ -267,7 +275,6 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {
nextCellText = '';
nextFillText = cellFormat;
break;

// =============== Arrows ===============
// Left key
case 'ArrowLeft':
Expand Down
10 changes: 8 additions & 2 deletions src/PickerInput/Selector/SingleSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,14 @@ function SingleSelector<DateType extends object = any>(
const rootProps = useRootProps(restProps);

// ======================== Change ========================
const onSingleChange = (date: DateType) => {
onChange([date]);
const onSingleChange = (date: DateType | null) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently only Single mode?
RangePicker seems also need have same behavior

if (date === null && clearIcon) {
// Only allow manual clear when clearIcon is present (allowClear was enabled)
onClear?.();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onClear is not optional

} else if (date !== null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe check date first and then can clear

onChange([date]);
}
// If date is null but clearIcon is not set, do nothing - let it reset to previous value
};

const onMultipleRemove = (date: DateType) => {
Expand Down
8 changes: 7 additions & 1 deletion src/PickerInput/Selector/hooks/useInputProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,14 @@ export default function useInputProps<DateType extends object = any>(
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: () => {
Expand Down
304 changes: 304 additions & 0 deletions tests/manual-clear.spec.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChange}
locale={enUS}
allowClear
/>,
);

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(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChange}
locale={enUS}
allowClear={false}
/>,
);

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(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChange}
locale={enUS}
format="YYYY-MM-DD"
/>,
);
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(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChange}
locale={enUS}
picker="month"
allowClear
/>,
);

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(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChange}
locale={enUS}
allowClear
/>,
);

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(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChange}
locale={enUS}
format={{ type: 'mask', format: 'YYYY-MM-DD' }}
allowClear
/>,
);

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(
<RangePicker
generateConfig={dayGenerateConfig}
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
onChange={onChange}
locale={enUS}
needConfirm={false}
allowClear
/>,
);

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(
<RangePicker
generateConfig={dayGenerateConfig}
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
onChange={onChange}
locale={enUS}
needConfirm={false}
allowClear
/>,
);

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(
<RangePicker
generateConfig={dayGenerateConfig}
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
onChange={onChange}
locale={enUS}
needConfirm={false}
allowClear
/>,
);

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(
<RangePicker
generateConfig={dayGenerateConfig}
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
onChange={onChange}
locale={enUS}
allowClear
/>,
);

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(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChangeManual}
locale={enUS}
allowClear
/>,
);

const input1 = container1.querySelector('input') as HTMLInputElement;
openPicker(container1);
fireEvent.change(input1, { target: { value: '' } });
await waitFakeTimer();

const { container: container2 } = render(
<Picker
generateConfig={dayGenerateConfig}
value={getDay('2023-08-01')}
onChange={onChangeClear}
locale={enUS}
allowClear
/>,
);

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);
});
});
});