diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 8cccd7869..387baba84 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -343,6 +343,7 @@ function RangePicker( setInnerValue, getCalendarValue, triggerCalendarChange, + true, // rangeValue disabled, formatList, focused, diff --git a/src/PickerInput/SinglePicker.tsx b/src/PickerInput/SinglePicker.tsx index 5b7f0a15e..b87059a4c 100644 --- a/src/PickerInput/SinglePicker.tsx +++ b/src/PickerInput/SinglePicker.tsx @@ -30,6 +30,7 @@ import useShowNow from './hooks/useShowNow'; import Popup from './Popup'; import SingleSelector from './Selector/SingleSelector'; import useSemantic from '../hooks/useSemantic'; +import { isSameTimestamp } from '../utils/dateUtil'; // TODO: isInvalidateDate with showTime.disabledTime should not provide `range` prop @@ -297,7 +298,8 @@ function Picker( setInnerValue, getCalendarValue, triggerCalendarChange, - [], //disabled, + false, // rangeValue + [], // disabled formatList, focused, mergedOpen, @@ -451,6 +453,18 @@ function Picker( onSetHover(date, 'cell'); }; + const isPopupInvalidateDate = useEvent((date: DateType) => { + if ( + multiple && + Array.isArray(mergedValue) && + mergedValue.some((valueDate) => isSameTimestamp(generateConfig, valueDate, date)) + ) { + return false; + } + + return isInvalidateDate(date, { activeIndex: 0 }); + }); + // >>> Focus const onPanelFocus: React.FocusEventHandler = (event) => { triggerOpen(true); @@ -527,7 +541,7 @@ function Picker( // Value format={maskFormat} value={calendarValue} - isInvalid={isInvalidateDate} + isInvalid={isPopupInvalidateDate} onChange={null} onSelect={onPanelSelect} // PickerValue diff --git a/src/PickerInput/hooks/useRangeValue.ts b/src/PickerInput/hooks/useRangeValue.ts index 6cd0ac367..4ab017e40 100644 --- a/src/PickerInput/hooks/useRangeValue.ts +++ b/src/PickerInput/hooks/useRangeValue.ts @@ -72,6 +72,17 @@ function orderDates( return [...dates].sort((a, b) => (generateConfig.isAfter(a, b) ? 1 : -1)) as DatesType; } +function includesTimestamp( + generateConfig: GenerateConfig, + dates: DateType[], + date: DateType, +) { + return ( + Array.isArray(dates) && + dates.some((prevDate) => isSameTimestamp(generateConfig, prevDate, date)) + ); +} + /** * Used for internal value management. * It should always use `mergedValue` in render logic @@ -171,6 +182,7 @@ export default function useRangeValue void, getCalendarValue: () => ValueType, triggerCalendarChange: TriggerCalendarChange, + rangeValue: boolean, disabled: ReplaceListType, boolean>, formatList: FormatType[], focused: boolean, @@ -263,11 +275,55 @@ export default function useRangeValue>> Invalid - const validateDates = - // Validate start - (disabled[0] || !start || !isInvalidateDate(start, { activeIndex: 0 })) && - // Validate end - (disabled[1] || !end || !isInvalidateDate(end, { from: start, activeIndex: 1 })); + const prevStart = mergedValue[0] || null; + const prevEnd = mergedValue[1] || null; + + const startChanged = !isSameTimestamp(generateConfig, prevStart, start || null); + const endChanged = !isSameTimestamp(generateConfig, prevEnd, end || null); + + // `validateDates` negates this helper: only a changed/new invalid date + // should block submission, while an unchanged value that was already + // invalid can remain without blocking another field update. + const isInvalidateChangedDate = ( + date: DateType, + prevDate: DateType, + changed: boolean, + prevInfo: { from?: DateType; activeIndex: number }, + nextInfo: { from?: DateType; activeIndex: number }, + ) => { + const nextInvalidate = isInvalidateDate(date, nextInfo); + + return nextInvalidate && (changed || !prevDate || !isInvalidateDate(prevDate, prevInfo)); + }; + + const validateDates = rangeValue + ? // Validate range dates. Existing invalid values should not block other updates. + (disabled[0] || + !start || + !isInvalidateChangedDate( + start, + prevStart, + startChanged, + { activeIndex: 0 }, + { activeIndex: 0 }, + )) && + (disabled[1] || + !end || + !isInvalidateChangedDate( + end, + prevEnd, + endChanged, + { from: prevStart, activeIndex: 1 }, + { from: start, activeIndex: 1 }, + )) + : // Validate single or multiple dates. Existing values may be disabled by updated `disabledDate`. + clone.every( + (date) => + !date || + includesTimestamp(generateConfig, mergedValue, date) || + !isInvalidateDate(date, { activeIndex: 0 }), + ); + // >>> Result const allPassed = // Null value is from clear button diff --git a/tests/multiple.spec.tsx b/tests/multiple.spec.tsx index fee0cd103..cc4505ac0 100644 --- a/tests/multiple.spec.tsx +++ b/tests/multiple.spec.tsx @@ -76,6 +76,44 @@ describe('Picker.Multiple', () => { expect(onChange).toHaveBeenCalledWith(expect.anything(), ['1990-09-01', '1990-09-05']); }); + it('does not block change when existing value is disabled by disabledDate', () => { + const onChange = jest.fn(); + const { container } = render( + date < getDay('1990-09-03').endOf('day')} + onChange={onChange} + />, + ); + + openPicker(container); + selectCell(5); + fireEvent.click(document.querySelector('.rc-picker-ok button')); + + expect(onChange).toHaveBeenCalledWith(expect.anything(), ['1990-09-03', '1990-09-05']); + }); + + it('blocks adding a preset value disabled by disabledDate', () => { + const onChange = jest.fn(); + const { container } = render( + date.isSame(getDay('1990-09-05'), 'date')} + presets={[{ label: 'Disabled', value: getDay('1990-09-05') }]} + onChange={onChange} + />, + ); + + openPicker(container); + fireEvent.click(document.querySelector('.rc-picker-presets li')); + + expect(onChange).not.toHaveBeenCalled(); + }); + it('selector remove', () => { const onChange = jest.fn(); const { container } = render( diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 18de5e82e..f75762db0 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -275,6 +275,99 @@ describe('Picker.Range', () => { ); }); + it('does not block change when existing value is disabled by disabledDate', () => { + const onChange = jest.fn(); + const { container } = render( + date < getDay('1990-09-03').endOf('day')} + onChange={onChange} + />, + ); + + openPicker(container, 1); + selectCell(6); + closePicker(container, 1); + + expect(onChange).toHaveBeenCalledWith( + [expect.anything(), expect.anything()], + ['1990-09-03', '1990-09-06'], + ); + }); + + it('does not block start change when existing end value is disabled by disabledDate', () => { + const onChange = jest.fn(); + const { container } = render( + date.isSame(getDay('1990-09-03'), 'date')} + onChange={onChange} + />, + ); + + openPicker(container, 0); + selectCell(2); + closePicker(container, 1); + + expect(onChange).toHaveBeenCalledWith( + [expect.anything(), expect.anything()], + ['1990-09-02', '1990-09-03'], + ); + }); + + it('blocks start change from disabled date to another disabled date', () => { + const onChange = jest.fn(); + const { container } = render( + date.isBefore(getDay('1990-09-03'), 'date')} + onChange={onChange} + />, + ); + + openPicker(container, 0); + inputValue('1990-09-02', 0); + keyDown(container, 0, KeyCode.ENTER); + closePicker(container, 0); + + expect(onChange).not.toHaveBeenCalled(); + }); + + it('blocks end change from disabled date to another disabled date', () => { + const onChange = jest.fn(); + const { container } = render( + !!from && date.isAfter(from.add(1, 'day'))} + onChange={onChange} + />, + ); + + openPicker(container, 1); + inputValue('1990-09-04', 1); + keyDown(container, 1, KeyCode.ENTER); + closePicker(container, 1); + + expect(onChange).not.toHaveBeenCalled(); + }); + + it('blocks change when existing end value becomes disabled by new start value', () => { + const onChange = jest.fn(); + const { container } = render( + !!from && date.isAfter(from.add(1, 'day'))} + onChange={onChange} + />, + ); + + openPicker(container, 0); + selectCell(2); + closePicker(container, 1); + + expect(onChange).not.toHaveBeenCalled(); + }); + it('should close panel when finish first choose with showTime = true and disabled = [false, true]', () => { const { baseElement } = render(); expect(baseElement.querySelectorAll('.rc-picker-input')).toHaveLength(2);