Skip to content

Commit d5bba8d

Browse files
committed
fix: trigger onChange when manually clearing input via select all + delete
When users manually select all text (Ctrl+A) and press Delete/Backspace, the onChange callback is now properly triggered with null, matching the behavior of the clear button. Previously, the input would reset to the previous value on blur without triggering onChange, which was inconsistent and prevented users from clearing dates via keyboard. Changes: - Modified useInputProps to detect empty input and trigger onChange(null) - Added handlers in Input component for both formatted and non-formatted inputs - Updated SingleSelector to properly handle null values by calling onClear - Added comprehensive test coverage with 11 new tests Fixes: #52473 (ant-design/ant-design) All existing tests pass (453 tests), ensuring backward compatibility.
1 parent 0dadb0a commit d5bba8d

File tree

4 files changed

+347
-2
lines changed

4 files changed

+347
-2
lines changed

src/PickerInput/Selector/Input.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,23 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {
213213

214214
// ======================= Keyboard =======================
215215
const onSharedKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
216+
const { key } = event;
217+
218+
if ((key === 'Backspace' || key === 'Delete') && !format) {
219+
const inputElement = inputRef.current;
220+
if (
221+
inputElement &&
222+
inputElement.selectionStart === 0 &&
223+
inputElement.selectionEnd === inputValue.length &&
224+
inputValue.length > 0
225+
) {
226+
onChange('');
227+
setInputValue('');
228+
event.preventDefault();
229+
return;
230+
}
231+
}
232+
216233
if (event.key === 'Enter' && validateFormat(inputValue)) {
217234
onSubmit();
218235
}
@@ -264,6 +281,18 @@ const Input = React.forwardRef<InputRef, InputProps>((props, ref) => {
264281
// =============== Remove ===============
265282
case 'Backspace':
266283
case 'Delete':
284+
const inputElement = inputRef.current;
285+
if (
286+
inputElement &&
287+
inputElement.selectionStart === 0 &&
288+
inputElement.selectionEnd === inputValue.length &&
289+
inputValue.length > 0
290+
) {
291+
onChange('');
292+
setInputValue('');
293+
event.preventDefault();
294+
return;
295+
}
267296
nextCellText = '';
268297
nextFillText = cellFormat;
269298
break;

src/PickerInput/Selector/SingleSelector/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ function SingleSelector<DateType extends object = any>(
131131

132132
// ======================== Change ========================
133133
const onSingleChange = (date: DateType) => {
134-
onChange([date]);
134+
if (date === null) {
135+
onClear?.();
136+
} else {
137+
onChange([date]);
138+
}
135139
};
136140

137141
const onMultipleRemove = (date: DateType) => {

src/PickerInput/Selector/hooks/useInputProps.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,13 @@ export default function useInputProps<DateType extends object = any>(
185185
return;
186186
}
187187

188+
if (text === '') {
189+
onInvalid(false, index);
190+
onChange(null, index);
191+
return;
192+
}
193+
188194
// Tell outer that the value typed is invalid.
189-
// If text is empty, it means valid.
190195
onInvalid(!!text, index);
191196
},
192197
onHelp: () => {

tests/manual-clear.spec.tsx

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import { fireEvent, render } from '@testing-library/react';
2+
import React from 'react';
3+
import { Picker, RangePicker } from '../src';
4+
import dayGenerateConfig from '../src/generate/dayjs';
5+
import enUS from '../src/locale/en_US';
6+
import { getDay, openPicker, waitFakeTimer } from './util/commonUtil';
7+
8+
describe('Picker.ManualClear', () => {
9+
beforeEach(() => {
10+
jest.useFakeTimers().setSystemTime(getDay('1990-09-03 00:00:00').valueOf());
11+
});
12+
13+
afterEach(() => {
14+
jest.clearAllTimers();
15+
jest.useRealTimers();
16+
});
17+
18+
describe('Single Picker', () => {
19+
it('should trigger onChange when manually clearing input (select all + delete)', async () => {
20+
const onChange = jest.fn();
21+
const { container } = render(
22+
<Picker
23+
generateConfig={dayGenerateConfig}
24+
value={getDay('2023-08-01')}
25+
onChange={onChange}
26+
locale={enUS}
27+
/>,
28+
);
29+
30+
const input = container.querySelector('input') as HTMLInputElement;
31+
32+
openPicker(container);
33+
input.setSelectionRange(0, input.value.length);
34+
fireEvent.keyDown(input, { key: 'Delete', code: 'Delete' });
35+
36+
await waitFakeTimer();
37+
38+
expect(onChange).toHaveBeenCalledWith(null, null);
39+
});
40+
41+
it('should trigger onChange when manually clearing input (select all + backspace)', async () => {
42+
const onChange = jest.fn();
43+
const { container } = render(
44+
<Picker
45+
generateConfig={dayGenerateConfig}
46+
value={getDay('2023-08-01')}
47+
onChange={onChange}
48+
locale={enUS}
49+
/>,
50+
);
51+
52+
const input = container.querySelector('input') as HTMLInputElement;
53+
54+
openPicker(container);
55+
input.setSelectionRange(0, input.value.length);
56+
fireEvent.keyDown(input, { key: 'Backspace', code: 'Backspace' });
57+
58+
await waitFakeTimer();
59+
60+
expect(onChange).toHaveBeenCalledWith(null, null);
61+
});
62+
63+
it('should trigger onChange when manually clearing via input change event', async () => {
64+
const onChange = jest.fn();
65+
const { container } = render(
66+
<Picker
67+
generateConfig={dayGenerateConfig}
68+
value={getDay('2023-08-01')}
69+
onChange={onChange}
70+
locale={enUS}
71+
/>,
72+
);
73+
74+
const input = container.querySelector('input') as HTMLInputElement;
75+
76+
openPicker(container);
77+
fireEvent.change(input, { target: { value: '' } });
78+
79+
await waitFakeTimer();
80+
81+
expect(onChange).toHaveBeenCalledWith(null, null);
82+
});
83+
84+
it('should reset invalid partial input on blur without triggering onChange', async () => {
85+
const onChange = jest.fn();
86+
const { container } = render(
87+
<Picker
88+
generateConfig={dayGenerateConfig}
89+
value={getDay('2023-08-01')}
90+
onChange={onChange}
91+
locale={enUS}
92+
format="YYYY-MM-DD"
93+
/>,
94+
);
95+
96+
const input = container.querySelector('input') as HTMLInputElement;
97+
98+
openPicker(container);
99+
100+
const initialOnChangeCallCount = onChange.mock.calls.length;
101+
102+
fireEvent.blur(input);
103+
await waitFakeTimer();
104+
105+
expect(onChange.mock.calls.length).toBe(initialOnChangeCallCount);
106+
expect(input.value).toBe('2023-08-01');
107+
});
108+
109+
it('should work with different picker modes', async () => {
110+
const onChange = jest.fn();
111+
const { container } = render(
112+
<Picker
113+
generateConfig={dayGenerateConfig}
114+
value={getDay('2023-08-01')}
115+
onChange={onChange}
116+
locale={enUS}
117+
picker="month"
118+
/>,
119+
);
120+
121+
const input = container.querySelector('input') as HTMLInputElement;
122+
123+
openPicker(container);
124+
input.setSelectionRange(0, input.value.length);
125+
fireEvent.keyDown(input, { key: 'Delete', code: 'Delete' });
126+
127+
await waitFakeTimer();
128+
129+
expect(onChange).toHaveBeenCalledWith(null, null);
130+
});
131+
132+
it('should clear input value when manually clearing', async () => {
133+
const onChange = jest.fn();
134+
const { container } = render(
135+
<Picker
136+
generateConfig={dayGenerateConfig}
137+
value={getDay('2023-08-01')}
138+
onChange={onChange}
139+
locale={enUS}
140+
/>,
141+
);
142+
143+
const input = container.querySelector('input') as HTMLInputElement;
144+
145+
expect(input.value).toBe('2023-08-01');
146+
147+
// Open picker
148+
openPicker(container);
149+
150+
// Simulate selecting all text and delete
151+
input.setSelectionRange(0, input.value.length);
152+
fireEvent.keyDown(input, { key: 'Delete', code: 'Delete' });
153+
154+
await waitFakeTimer();
155+
156+
// Input should be empty
157+
expect(input.value).toBe('');
158+
});
159+
});
160+
161+
describe('Range Picker', () => {
162+
it('should trigger onChange when manually clearing start input', async () => {
163+
const onChange = jest.fn();
164+
const { container } = render(
165+
<RangePicker
166+
generateConfig={dayGenerateConfig}
167+
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
168+
onChange={onChange}
169+
locale={enUS}
170+
needConfirm={false}
171+
/>,
172+
);
173+
174+
const startInput = container.querySelectorAll('input')[0] as HTMLInputElement;
175+
176+
openPicker(container, 0);
177+
startInput.setSelectionRange(0, startInput.value.length);
178+
fireEvent.keyDown(startInput, { key: 'Delete', code: 'Delete' });
179+
fireEvent.blur(startInput);
180+
181+
await waitFakeTimer();
182+
183+
expect(startInput.value).toBe('');
184+
});
185+
186+
it('should trigger onChange when manually clearing end input', async () => {
187+
const onChange = jest.fn();
188+
const { container } = render(
189+
<RangePicker
190+
generateConfig={dayGenerateConfig}
191+
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
192+
onChange={onChange}
193+
locale={enUS}
194+
needConfirm={false}
195+
/>,
196+
);
197+
198+
const endInput = container.querySelectorAll('input')[1] as HTMLInputElement;
199+
200+
openPicker(container, 1);
201+
endInput.setSelectionRange(0, endInput.value.length);
202+
fireEvent.keyDown(endInput, { key: 'Delete', code: 'Delete' });
203+
fireEvent.blur(endInput);
204+
205+
await waitFakeTimer();
206+
207+
expect(endInput.value).toBe('');
208+
});
209+
210+
it('should trigger onChange when manually clearing both inputs', async () => {
211+
const onChange = jest.fn();
212+
const { container } = render(
213+
<RangePicker
214+
generateConfig={dayGenerateConfig}
215+
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
216+
onChange={onChange}
217+
locale={enUS}
218+
needConfirm={false}
219+
/>,
220+
);
221+
222+
const startInput = container.querySelectorAll('input')[0] as HTMLInputElement;
223+
const endInput = container.querySelectorAll('input')[1] as HTMLInputElement;
224+
225+
openPicker(container, 0);
226+
startInput.setSelectionRange(0, startInput.value.length);
227+
fireEvent.keyDown(startInput, { key: 'Delete', code: 'Delete' });
228+
fireEvent.blur(startInput);
229+
await waitFakeTimer();
230+
231+
openPicker(container, 1);
232+
endInput.setSelectionRange(0, endInput.value.length);
233+
fireEvent.keyDown(endInput, { key: 'Delete', code: 'Delete' });
234+
fireEvent.blur(endInput);
235+
await waitFakeTimer();
236+
237+
expect(startInput.value).toBe('');
238+
expect(endInput.value).toBe('');
239+
});
240+
241+
it('should clear input values when manually clearing', async () => {
242+
const onChange = jest.fn();
243+
const { container } = render(
244+
<RangePicker
245+
generateConfig={dayGenerateConfig}
246+
value={[getDay('2023-08-01'), getDay('2023-08-15')]}
247+
onChange={onChange}
248+
locale={enUS}
249+
/>,
250+
);
251+
252+
const startInput = container.querySelectorAll('input')[0] as HTMLInputElement;
253+
254+
expect(startInput.value).toBe('2023-08-01');
255+
256+
openPicker(container, 0);
257+
startInput.setSelectionRange(0, startInput.value.length);
258+
fireEvent.keyDown(startInput, { key: 'Delete', code: 'Delete' });
259+
260+
await waitFakeTimer();
261+
262+
expect(startInput.value).toBe('');
263+
});
264+
});
265+
266+
describe('Comparison with clear button', () => {
267+
it('manual clear should behave the same as clear button for Picker', async () => {
268+
const onChangeManual = jest.fn();
269+
const onChangeClear = jest.fn();
270+
271+
const { container: container1 } = render(
272+
<Picker
273+
generateConfig={dayGenerateConfig}
274+
value={getDay('2023-08-01')}
275+
onChange={onChangeManual}
276+
locale={enUS}
277+
allowClear
278+
/>,
279+
);
280+
281+
const input1 = container1.querySelector('input') as HTMLInputElement;
282+
openPicker(container1);
283+
input1.setSelectionRange(0, input1.value.length);
284+
fireEvent.keyDown(input1, { key: 'Delete', code: 'Delete' });
285+
await waitFakeTimer();
286+
287+
const { container: container2 } = render(
288+
<Picker
289+
generateConfig={dayGenerateConfig}
290+
value={getDay('2023-08-01')}
291+
onChange={onChangeClear}
292+
locale={enUS}
293+
allowClear
294+
/>,
295+
);
296+
297+
const clearBtn = container2.querySelector('.rc-picker-clear');
298+
fireEvent.mouseDown(clearBtn);
299+
fireEvent.mouseUp(clearBtn);
300+
fireEvent.click(clearBtn);
301+
await waitFakeTimer();
302+
303+
expect(onChangeManual).toHaveBeenCalledWith(null, null);
304+
expect(onChangeClear).toHaveBeenCalledWith(null, null);
305+
});
306+
});
307+
});

0 commit comments

Comments
 (0)