diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 4daadcae0..5e667b2d6 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) { @@ -223,6 +226,7 @@ function RangePicker( // Native onClick, + onMouseDown, } = filledProps; // ========================= Refs ========================= @@ -269,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); @@ -666,6 +672,9 @@ function RangePicker( return; } + keyboardSwitchInputRef.current = pendingKeyboardSwitchRef.current; + pendingKeyboardSwitchRef.current = false; + lastOperation('input'); triggerOpen(true, { @@ -685,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); @@ -694,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); } @@ -739,7 +771,8 @@ function RangePicker( const lastOp = lastOperation(); // Trade as confirm on field leave - if (!mergedOpen && lastOp === 'input') { + if (!mergedOpen && lastOp === 'input' && (!needConfirm || keyboardSwitchInputRef.current)) { + keyboardSwitchInputRef.current = false; triggerOpen(false); triggerPartConfirm(null, true); } @@ -822,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 4a500236b..ae96ed357 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -895,6 +895,230 @@ 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 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); + + 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, '', ''); + }); + + 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, '', ''); + }); + + 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 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(); + + 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);