diff --git a/src/RangePicker.tsx b/src/RangePicker.tsx index 9421e8585..909bfcd39 100644 --- a/src/RangePicker.tsx +++ b/src/RangePicker.tsx @@ -275,6 +275,11 @@ function InnerRangePicker(props: RangePickerProps) { // ============================= Misc ============================== const formatList = toArray(getDefaultFormat(format, picker, showTime, use12Hours)); + const formatDateValue = (values: RangeValue, index: 0 | 1) => + values && values[index] + ? formatValue(values[index], { generateConfig, locale, format: formatList[0] }) + : ''; + // Operation ref const operationRef: React.MutableRefObject = useRef(null); @@ -394,7 +399,11 @@ function InnerRangePicker(props: RangePickerProps) { }, 0); } - function triggerChange(newValue: RangeValue, sourceIndex: 0 | 1) { + function triggerChange( + newValue: RangeValue, + sourceIndex: 0 | 1, + triggerCalendarChangeOnly?: boolean, + ) { let values = newValue; let startValue = getValue(values, 0); let endValue = getValue(values, 1); @@ -428,14 +437,8 @@ function InnerRangePicker(props: RangePickerProps) { setSelectedValue(values); - const startStr = - values && values[0] - ? formatValue(values[0], { generateConfig, locale, format: formatList[0] }) - : ''; - const endStr = - values && values[1] - ? formatValue(values[1], { generateConfig, locale, format: formatList[0] }) - : ''; + const startStr = formatDateValue(values, 0); + const endStr = formatDateValue(values, 1); if (onCalendarChange) { const info: RangeInfo = { range: sourceIndex === 0 ? 'start' : 'end' }; @@ -443,22 +446,21 @@ function InnerRangePicker(props: RangePickerProps) { onCalendarChange(values, [startStr, endStr], info); } - // >>>>> Trigger `onChange` event - const canStartValueTrigger = canValueTrigger(startValue, 0, mergedDisabled, allowEmpty); - const canEndValueTrigger = canValueTrigger(endValue, 1, mergedDisabled, allowEmpty); - - const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); - - if (canTrigger) { - // Trigger onChange only when value is validate - setInnerValue(values); - - if ( - onChange && - (!isEqual(generateConfig, getValue(mergedValue, 0), startValue) || - !isEqual(generateConfig, getValue(mergedValue, 1), endValue)) - ) { - onChange(values, [startStr, endStr]); + if (!triggerCalendarChangeOnly) { + // >>>>> Trigger `onChange` event + const canStartValueTrigger = canValueTrigger(startValue, 0, mergedDisabled, allowEmpty); + const canEndValueTrigger = canValueTrigger(endValue, 1, mergedDisabled, allowEmpty); + const canTrigger = values === null || (canStartValueTrigger && canEndValueTrigger); + if (canTrigger) { + // Trigger onChange only when value is validate + setInnerValue(values); + if ( + onChange && + (!isEqual(generateConfig, getValue(mergedValue, 0), startValue) || + !isEqual(generateConfig, getValue(mergedValue, 1), endValue)) + ) { + onChange(values, [startStr, endStr]); + } } } } @@ -566,12 +568,26 @@ function InnerRangePicker(props: RangePickerProps) { }, [mergedOpen]); const onInternalBlur: React.FocusEventHandler = (e) => { - if (changeOnBlur && delayOpen) { - const selectedIndexValue = getValue(selectedValue, mergedActivePickerIndex); - if (selectedIndexValue) { - triggerChange(selectedValue, mergedActivePickerIndex); + if (delayOpen) { + if (changeOnBlur) { + const selectedIndexValue = getValue(selectedValue, mergedActivePickerIndex); + + if (selectedIndexValue) { + triggerChange(selectedValue, mergedActivePickerIndex); + } + } else if (needConfirmButton) { + // when in dateTime mode, switching between two date input fields will trigger onCalendarChange. + // when onBlur is triggered, the input field has already switched, + // so it's necessary to obtain the value of the previous input field here. + const needTriggerIndex = mergedActivePickerIndex ? 0 : 1; + const selectedIndexValue = getValue(selectedValue, needTriggerIndex); + + if (selectedIndexValue) { + triggerChange(selectedValue, needTriggerIndex, true); + } } } + return onBlur?.(e); }; @@ -579,16 +595,17 @@ function InnerRangePicker(props: RangePickerProps) { blurToCancel: !changeOnBlur && needConfirmButton, forwardKeyDown, onBlur: onInternalBlur, - isClickOutside: (target: EventTarget | null) => - !elementsContains( + isClickOutside: (target: EventTarget | null) => { + const elementsRefs = [startInputDivRef.current, endInputDivRef.current, containerRef.current]; + return !elementsContains( [ + // Filter the ref of the currently selected input to trigger the onBlur event of another input. + ...(needConfirmButton ? [elementsRefs[mergedActivePickerIndex]] : elementsRefs), panelDivRef.current, - startInputDivRef.current, - endInputDivRef.current, - containerRef.current, ], target as HTMLElement, - ), + ); + }, onFocus: (e: React.FocusEvent) => { if (onFocus) { onFocus(e); @@ -633,7 +650,6 @@ function InnerRangePicker(props: RangePickerProps) { onKeyDown: (e, preventDefault) => { onKeyDown?.(e, preventDefault); }, - changeOnBlur, }; const [startInputProps, { focused: startFocused, typing: startTyping }] = usePickerInput({ diff --git a/src/hooks/usePickerInput.ts b/src/hooks/usePickerInput.ts index 5e0ffac65..6aec55bc2 100644 --- a/src/hooks/usePickerInput.ts +++ b/src/hooks/usePickerInput.ts @@ -16,7 +16,6 @@ export default function usePickerInput({ onCancel, onFocus, onBlur, - changeOnBlur, }: { open: boolean; value: string; @@ -29,7 +28,6 @@ export default function usePickerInput({ onCancel: () => void; onFocus?: React.FocusEventHandler; onBlur?: React.FocusEventHandler; - changeOnBlur?: boolean; }): [React.DOMAttributes, { focused: boolean; typing: boolean }] { const [typing, setTyping] = useState(false); const [focused, setFocused] = useState(false); @@ -160,7 +158,7 @@ export default function usePickerInput({ raf(() => { preventBlurRef.current = false; }); - } else if (!changeOnBlur && (!focused || clickedOutside)) { + } else if (!blurToCancel && (!focused || clickedOutside)) { triggerOpen(false); } } diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 93777a925..52e732b29 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -3,9 +3,11 @@ import type { Moment } from 'moment'; import moment from 'moment'; import KeyCode from 'rc-util/lib/KeyCode'; import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; +import { resetWarned } from 'rc-util/lib/warning'; import React from 'react'; import type { PickerMode } from '../src/interface'; import zhCN from '../src/locale/zh_CN'; +import type { RangePickerProps } from '../src/RangePicker'; import { clearValue, clickButton, @@ -19,8 +21,6 @@ import { openPicker, selectCell, } from './util/commonUtil'; -import type { RangePickerProps } from '../src/RangePicker'; -import { resetWarned } from 'rc-util/lib/warning'; describe('Picker.Range', () => { let errorSpy; @@ -286,7 +286,9 @@ describe('Picker.Range', () => { expect(baseElement.querySelectorAll('.rc-picker-input')).toHaveLength(2); fireEvent.click(baseElement.querySelectorAll('.rc-picker-input')[1]); expect(baseElement.querySelector('.rc-picker-dropdown-hidden')).toBeFalsy(); - fireEvent.click(baseElement.querySelector('.rc-picker-cell-range-start .rc-picker-cell-inner')); + fireEvent.click( + baseElement.querySelector('.rc-picker-cell-range-start .rc-picker-cell-inner'), + ); fireEvent.click(baseElement.querySelector('.rc-picker-ok button')); expect(baseElement.querySelector('.rc-picker-dropdown-hidden')).toBeTruthy(); }); @@ -361,7 +363,6 @@ describe('Picker.Range', () => { }); function testRangePickerPresetRange(propsType: 'ranges' | 'presets') { - const genProps = (ranges: Record) => { const props: Partial> = {}; if (propsType === 'ranges') { @@ -371,21 +372,19 @@ describe('Picker.Range', () => { props.presets = []; Object.entries(ranges).forEach(([label, value]) => { props.presets.push({ label, value }); - }) + }); } return props as RangePickerProps; - } + }; it(`${propsType} work`, () => { const onChange = jest.fn(); const { container } = render( [getMoment('2000-01-01'), getMoment('2010-11-11')], - } - )} + {...genProps({ + test: [getMoment('1989-11-28'), getMoment('1990-09-03')], + func: () => [getMoment('2000-01-01'), getMoment('2010-11-11')], + })} onChange={onChange} />, ); @@ -420,11 +419,9 @@ describe('Picker.Range', () => { it(`${propsType} hover className`, () => { const { container } = render( , ); @@ -439,7 +436,6 @@ describe('Picker.Range', () => { expect(findCell(12)).not.toHaveClass('rc-picker-cell-in-range'); expect(findCell(13)).not.toHaveClass('rc-picker-cell-range-end'); }); - } describe('ranges or presets', () => { @@ -755,7 +751,7 @@ describe('Picker.Range', () => { expect(container).toMatchSnapshot(); expect(errorSpy).toHaveBeenCalledWith( - 'Warning: `clearIcon` will be removed in future. Please use `allowClear` instead.' + 'Warning: `clearIcon` will be removed in future. Please use `allowClear` instead.', ); }); @@ -1111,19 +1107,19 @@ describe('Picker.Range', () => { targetCell: string; match: string[]; }[] = [ - { - picker: 'week', - defaultValue: ['2020-06-13'], - targetCell: '9', - match: ['2020-24th'], - }, - { - picker: 'quarter', - defaultValue: ['2020-03-30', '2020-05-20'], - targetCell: 'Q1', - match: ['2020-Q1'], - }, - ]; + { + picker: 'week', + defaultValue: ['2020-06-13'], + targetCell: '9', + match: ['2020-24th'], + }, + { + picker: 'quarter', + defaultValue: ['2020-03-30', '2020-05-20'], + targetCell: 'Q1', + match: ['2020-Q1'], + }, + ]; list.forEach(({ picker, defaultValue, match, targetCell }) => { it(picker, () => { @@ -1927,4 +1923,25 @@ describe('Picker.Range', () => { fireEvent.click(document.querySelector('.rc-picker-cell')); expect(document.querySelectorAll('.rc-picker-input')[1]).toHaveClass('rc-picker-input-active'); }); + + it('dateTime mode switch should trigger onCalendarChange', () => { + const onCalendarChange = jest.fn(); + const { container } = render( + , + ); + + openPicker(container, 0); + + selectCell(8, 0); + + openPicker(container, 1); + + // onBlur is triggered when the switch is complete + closePicker(container, 0); + + expect(onCalendarChange).toHaveBeenCalled(); + }); });