From afeee059194f108bb550aa4b76f0d10c5896271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Thu, 11 Jun 2026 14:27:58 +0800 Subject: [PATCH 1/2] fix: allow picker changes with disabled existing values --- src/PickerInput/SinglePicker.tsx | 14 +++++- src/PickerInput/hooks/useRangeValue.ts | 60 +++++++++++++++++++++++--- tests/multiple.spec.tsx | 19 ++++++++ tests/range.spec.tsx | 57 ++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 6 deletions(-) diff --git a/src/PickerInput/SinglePicker.tsx b/src/PickerInput/SinglePicker.tsx index 5b7f0a15e..2609a54b9 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 @@ -451,6 +452,17 @@ function Picker( onSetHover(date, 'cell'); }; + const isPopupInvalidateDate = useEvent((date: DateType) => { + if ( + multiple && + mergedValue.some((valueDate) => isSameTimestamp(generateConfig, valueDate, date)) + ) { + return false; + } + + return isInvalidateDate(date, { activeIndex: 0 }); + }); + // >>> Focus const onPanelFocus: React.FocusEventHandler = (event) => { triggerOpen(true); @@ -527,7 +539,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..1442be762 100644 --- a/src/PickerInput/hooks/useRangeValue.ts +++ b/src/PickerInput/hooks/useRangeValue.ts @@ -72,6 +72,14 @@ function orderDates( return [...dates].sort((a, b) => (generateConfig.isAfter(a, b) ? 1 : -1)) as DatesType; } +function includesTimestamp( + generateConfig: GenerateConfig, + dates: DateType[], + date: DateType, +) { + return dates.some((prevDate) => isSameTimestamp(generateConfig, prevDate, date)); +} + /** * Used for internal value management. * It should always use `mergedValue` in render logic @@ -197,6 +205,7 @@ export default function useRangeValue d) ? false : order; + const isRangeValue = disabled.length > 0; // ============================= Util ============================= const [getDateTexts, isSameDates] = useUtil(generateConfig, locale, formatList); @@ -263,11 +272,52 @@ 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); + + 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 = isRangeValue + ? // 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..224e2afe9 100644 --- a/tests/multiple.spec.tsx +++ b/tests/multiple.spec.tsx @@ -76,6 +76,25 @@ 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('selector remove', () => { const onChange = jest.fn(); const { container } = render( diff --git a/tests/range.spec.tsx b/tests/range.spec.tsx index 18de5e82e..2544a5053 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -275,6 +275,63 @@ 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 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); From f9eb791af522657ac1916e07a4216107c8a91530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Sat, 13 Jun 2026 19:22:01 +0800 Subject: [PATCH 2/2] test: cover disabled date validation review --- src/PickerInput/RangePicker.tsx | 1 + src/PickerInput/SinglePicker.tsx | 4 ++- src/PickerInput/hooks/useRangeValue.ts | 12 ++++++--- tests/multiple.spec.tsx | 19 ++++++++++++++ tests/range.spec.tsx | 36 ++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) 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 2609a54b9..b87059a4c 100644 --- a/src/PickerInput/SinglePicker.tsx +++ b/src/PickerInput/SinglePicker.tsx @@ -298,7 +298,8 @@ function Picker( setInnerValue, getCalendarValue, triggerCalendarChange, - [], //disabled, + false, // rangeValue + [], // disabled formatList, focused, mergedOpen, @@ -455,6 +456,7 @@ function Picker( const isPopupInvalidateDate = useEvent((date: DateType) => { if ( multiple && + Array.isArray(mergedValue) && mergedValue.some((valueDate) => isSameTimestamp(generateConfig, valueDate, date)) ) { return false; diff --git a/src/PickerInput/hooks/useRangeValue.ts b/src/PickerInput/hooks/useRangeValue.ts index 1442be762..4ab017e40 100644 --- a/src/PickerInput/hooks/useRangeValue.ts +++ b/src/PickerInput/hooks/useRangeValue.ts @@ -77,7 +77,10 @@ function includesTimestamp( dates: DateType[], date: DateType, ) { - return dates.some((prevDate) => isSameTimestamp(generateConfig, prevDate, date)); + return ( + Array.isArray(dates) && + dates.some((prevDate) => isSameTimestamp(generateConfig, prevDate, date)) + ); } /** @@ -179,6 +182,7 @@ export default function useRangeValue void, getCalendarValue: () => ValueType, triggerCalendarChange: TriggerCalendarChange, + rangeValue: boolean, disabled: ReplaceListType, boolean>, formatList: FormatType[], focused: boolean, @@ -205,7 +209,6 @@ export default function useRangeValue d) ? false : order; - const isRangeValue = disabled.length > 0; // ============================= Util ============================= const [getDateTexts, isSameDates] = useUtil(generateConfig, locale, formatList); @@ -278,6 +281,9 @@ export default function useRangeValue { 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 2544a5053..f75762db0 100644 --- a/tests/range.spec.tsx +++ b/tests/range.spec.tsx @@ -315,6 +315,42 @@ describe('Picker.Range', () => { ); }); + 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(