From 08a2e6389f1f52a34e7b44d8802a5ccc0ef719f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Mon, 20 Apr 2026 21:01:15 +0800 Subject: [PATCH 1/5] fix: avoid submitting unconfirmed range values on blur --- src/PickerInput/RangePicker.tsx | 13 +++++++++---- tests/range.spec.tsx | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 4daadcae0..a3636e817 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -56,8 +56,10 @@ export type RangeValueType = [ /** Used for change event, it should always be not undefined */ export type NoUndefinedRangeValueType = [start: DateType | null, end: DateType | null]; -export interface BaseRangePickerProps - extends Omit, 'showTime' | 'id'> { +export interface BaseRangePickerProps extends Omit< + SharedPickerProps, + 'showTime' | 'id' +> { // Structure id?: SelectorIdType; @@ -132,7 +134,8 @@ export interface BaseRangePickerProps } export interface RangePickerProps - extends BaseRangePickerProps, + extends + BaseRangePickerProps, Omit, 'format' | 'defaultValue' | 'defaultOpenValue'> {} function getActiveRange(activeIndex: number) { @@ -666,7 +669,9 @@ function RangePicker( return; } - lastOperation('input'); + if (!needConfirm) { + lastOperation('input'); + } triggerOpen(true, { inherit: true, diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 4a500236b..af5398340 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -895,6 +895,29 @@ describe('Picker.Range', () => { expect(document.querySelector('input').value).toEqual(''); }); + it('should not submit unconfirmed values on blur when allowEmpty lets fields switch', () => { + const onChange = jest.fn(); + const { container } = render(); + + openPicker(container, 0); + selectCell(11); + + openPicker(container, 1); + openPicker(container, 0); + + fireEvent.mouseDown(document.body); + container.querySelectorAll('input')[0].blur(); + + for (let i = 0; i < 5; i += 1) { + act(() => { + jest.runAllTimers(); + }); + } + + expect(onChange).not.toHaveBeenCalled(); + matchValues(container, '', ''); + }); + describe('viewDate', () => { function matchTitle(title: string) { expect(document.querySelector('.rc-picker-header-view').textContent).toEqual(title); From 2e520bb744d713af334f0ca69d45a3f5c02ee16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Mon, 20 Apr 2026 21:28:43 +0800 Subject: [PATCH 2/5] fix: respect needConfirm for typed range input blur --- src/PickerInput/RangePicker.tsx | 2 +- tests/range.spec.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index a3636e817..a0398ad29 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -744,7 +744,7 @@ function RangePicker( const lastOp = lastOperation(); // Trade as confirm on field leave - if (!mergedOpen && lastOp === 'input') { + if (!mergedOpen && !needConfirm && lastOp === 'input') { triggerOpen(false); triggerPartConfirm(null, true); } diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index af5398340..3a960a99f 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -918,6 +918,32 @@ describe('Picker.Range', () => { matchValues(container, '', ''); }); + it('should not submit typed values on blur before confirm', () => { + const onChange = jest.fn(); + const { container } = render(); + + const startInput = container.querySelectorAll('input')[0]; + + startInput.focus(); + fireEvent.change(startInput, { + target: { + value: '1990-09-11 00:00:00', + }, + }); + + fireEvent.mouseDown(document.body); + startInput.blur(); + + for (let i = 0; i < 5; i += 1) { + act(() => { + jest.runAllTimers(); + }); + } + + expect(onChange).not.toHaveBeenCalled(); + matchValues(container, '', ''); + }); + describe('viewDate', () => { function matchTitle(title: string) { expect(document.querySelector('.rc-picker-header-view').textContent).toEqual(title); From 1581849810a42a5a11d9aa31736187f4823fd7ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Wed, 22 Apr 2026 18:13:58 +0800 Subject: [PATCH 3/5] fix: avoid confirming unsubmitted range blur values --- src/PickerInput/RangePicker.tsx | 4 +--- tests/range.spec.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index a0398ad29..c0e8069c8 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -669,9 +669,7 @@ function RangePicker( return; } - if (!needConfirm) { - lastOperation('input'); - } + lastOperation('input'); triggerOpen(true, { inherit: true, diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 3a960a99f..e2f4ec394 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -897,10 +897,21 @@ describe('Picker.Range', () => { it('should not submit unconfirmed values on blur when allowEmpty lets fields switch', () => { const onChange = jest.fn(); - const { container } = render(); + const onCalendarChange = jest.fn(); + const { container } = render( + , + ); openPicker(container, 0); selectCell(11); + expect(onCalendarChange).toHaveBeenCalledWith(expect.anything(), ['1990-09-11 00:00:00', ''], { + range: 'start', + }); openPicker(container, 1); openPicker(container, 0); From cc1730c18196ba7105300df65add9de89d97db6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Thu, 23 Apr 2026 14:10:25 +0800 Subject: [PATCH 4/5] fix: keep range blur confirm for keyboard field switch --- src/PickerInput/RangePicker.tsx | 33 ++++++++- tests/range.spec.tsx | 120 ++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index c0e8069c8..5e667b2d6 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -226,6 +226,7 @@ function RangePicker( // Native onClick, + onMouseDown, } = filledProps; // ========================= Refs ========================= @@ -272,6 +273,8 @@ function RangePicker( updateSubmitIndex, hasActiveSubmitValue, ] = useRangeActive(disabled, allowEmpty, mergedOpen); + const pendingKeyboardSwitchRef = React.useRef(false); + const keyboardSwitchInputRef = React.useRef(false); const onSharedFocus = (event: React.FocusEvent, index?: number) => { triggerFocus(true); @@ -669,6 +672,9 @@ function RangePicker( return; } + keyboardSwitchInputRef.current = pendingKeyboardSwitchRef.current; + pendingKeyboardSwitchRef.current = false; + lastOperation('input'); triggerOpen(true, { @@ -688,6 +694,14 @@ function RangePicker( }; const onSelectorBlur: SelectorProps['onBlur'] = (event, index) => { + const relatedTarget = event.relatedTarget as Node | null; + if ( + pendingKeyboardSwitchRef.current && + !selectorRef.current.nativeElement.contains(relatedTarget) + ) { + pendingKeyboardSwitchRef.current = false; + } + triggerOpen(false); if (!needConfirm && lastOperation() === 'input') { const nextIndex = nextActiveIndex(calendarValue); @@ -697,8 +711,23 @@ function RangePicker( onSharedBlur(event, index); }; + const onSelectorMouseDown: React.MouseEventHandler = (event) => { + const target = event.target as HTMLElement; + const rootNode = target.getRootNode(); + const activeElement = + (rootNode as Document | ShadowRoot).activeElement ?? document.activeElement; + + if (target.tagName === 'INPUT' && target !== activeElement) { + pendingKeyboardSwitchRef.current = false; + keyboardSwitchInputRef.current = false; + } + + onMouseDown?.(event); + }; + const onSelectorKeyDown: SelectorProps['onKeyDown'] = (event, preventDefault) => { if (event.key === 'Tab') { + pendingKeyboardSwitchRef.current = true; triggerPartConfirm(null, true); } @@ -742,7 +771,8 @@ function RangePicker( const lastOp = lastOperation(); // Trade as confirm on field leave - if (!mergedOpen && !needConfirm && lastOp === 'input') { + if (!mergedOpen && lastOp === 'input' && (!needConfirm || keyboardSwitchInputRef.current)) { + keyboardSwitchInputRef.current = false; triggerOpen(false); triggerPartConfirm(null, true); } @@ -825,6 +855,7 @@ function RangePicker( onOpenChange={triggerOpen} // Click onClick={onSelectorClick} + onMouseDown={onSelectorMouseDown} onClear={onSelectorClear} // Invalid invalid={submitInvalidates} diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index e2f4ec394..50c13cf3f 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -955,6 +955,126 @@ describe('Picker.Range', () => { matchValues(container, '', ''); }); + it('should submit typed values on blur after keyboard switch to next input', () => { + const onChange = jest.fn(); + const { container } = render(); + + const [startInput, endInput] = container.querySelectorAll('input'); + + startInput.focus(); + fireEvent.change(startInput, { + target: { + value: '1990-09-11 00:00:00', + }, + }); + fireEvent.keyDown(startInput, { + key: 'Tab', + }); + + endInput.focus(); + fireEvent.change(endInput, { + target: { + value: '1990-09-12 00:00:00', + }, + }); + + fireEvent.mouseDown(document.body); + endInput.blur(); + + for (let i = 0; i < 5; i += 1) { + act(() => { + jest.runAllTimers(); + }); + } + + expect(onChange).toHaveBeenCalledWith(expect.anything(), [ + '1990-09-11 00:00:00', + '1990-09-12 00:00:00', + ]); + matchValues(container, '1990-09-11 00:00:00', '1990-09-12 00:00:00'); + }); + + it('should not confirm typed end value on blur after mouse switching to next input', () => { + const onChange = jest.fn(); + const { container } = render(); + + const [startInput, endInput] = container.querySelectorAll('input'); + + startInput.focus(); + fireEvent.change(startInput, { + target: { + value: '1990-09-11 00:00:00', + }, + }); + fireEvent.keyDown(startInput, { + key: 'Tab', + }); + + fireEvent.mouseDown(endInput); + endInput.focus(); + fireEvent.change(endInput, { + target: { + value: '1990-09-12 00:00:00', + }, + }); + + fireEvent.mouseDown(document.body); + endInput.blur(); + + for (let i = 0; i < 5; i += 1) { + act(() => { + jest.runAllTimers(); + }); + } + + expect(onChange).not.toHaveBeenCalledWith(expect.anything(), [ + '1990-09-11 00:00:00', + '1990-09-12 00:00:00', + ]); + expect(onChange).toHaveBeenCalled(); + matchValues(container, '1990-09-11 00:00:00', ''); + }); + + it('should keep keyboard switch allowance when clicking inside the current input', () => { + const onChange = jest.fn(); + const { container } = render(); + + const [startInput, endInput] = container.querySelectorAll('input'); + + startInput.focus(); + fireEvent.change(startInput, { + target: { + value: '1990-09-11 00:00:00', + }, + }); + fireEvent.keyDown(startInput, { + key: 'Tab', + }); + + endInput.focus(); + fireEvent.change(endInput, { + target: { + value: '1990-09-12 00:00:00', + }, + }); + + fireEvent.mouseDown(endInput); + fireEvent.mouseDown(document.body); + endInput.blur(); + + for (let i = 0; i < 5; i += 1) { + act(() => { + jest.runAllTimers(); + }); + } + + expect(onChange).toHaveBeenCalledWith(expect.anything(), [ + '1990-09-11 00:00:00', + '1990-09-12 00:00:00', + ]); + matchValues(container, '1990-09-11 00:00:00', '1990-09-12 00:00:00'); + }); + describe('viewDate', () => { function matchTitle(title: string) { expect(document.querySelector('.rc-picker-header-view').textContent).toEqual(title); From 4d62aca607edbc9e46abb960e73194b248d38c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Sat, 25 Apr 2026 11:14:25 +0800 Subject: [PATCH 5/5] test: cover pending range keyboard switch cleanup --- tests/range.spec.tsx | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 50c13cf3f..ae96ed357 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -1035,6 +1035,50 @@ describe('Picker.Range', () => { matchValues(container, '1990-09-11 00:00:00', ''); }); + it('should clear pending keyboard switch when focus leaves picker before mouse switching', () => { + const onChange = jest.fn(); + const { container } = render(); + + const [startInput, endInput] = container.querySelectorAll('input'); + + startInput.focus(); + fireEvent.change(startInput, { + target: { + value: '1990-09-11 00:00:00', + }, + }); + fireEvent.keyDown(startInput, { + key: 'Tab', + }); + + fireEvent.blur(startInput, { + relatedTarget: document.body, + }); + fireEvent.mouseDown(endInput); + endInput.focus(); + fireEvent.change(endInput, { + target: { + value: '1990-09-12 00:00:00', + }, + }); + + fireEvent.mouseDown(document.body); + endInput.blur(); + + for (let i = 0; i < 5; i += 1) { + act(() => { + jest.runAllTimers(); + }); + } + + expect(onChange).not.toHaveBeenCalledWith(expect.anything(), [ + '1990-09-11 00:00:00', + '1990-09-12 00:00:00', + ]); + expect(onChange).toHaveBeenCalled(); + matchValues(container, '1990-09-11 00:00:00', ''); + }); + it('should keep keyboard switch allowance when clicking inside the current input', () => { const onChange = jest.fn(); const { container } = render();