From 3a75a7e66b7a1928fdcbabf2e74f61f11d9bcf4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Wed, 1 Jul 2026 08:59:38 +0800 Subject: [PATCH] Add range info to disabledDate --- src/PickerInput/RangePicker.tsx | 19 +++- src/PickerInput/hooks/useInvalidate.ts | 96 ++++++++++--------- src/PickerInput/hooks/useRangeDisabledDate.ts | 9 +- src/PickerInput/hooks/useRangeValue.ts | 3 +- src/interface.tsx | 11 ++- tests/new-range.spec.tsx | 89 +++++++++++++++++ 6 files changed, 169 insertions(+), 58 deletions(-) diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 8bf7d9e76..0e0f95451 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -37,6 +37,7 @@ import useRangeDisabledDate from './hooks/useRangeDisabledDate'; import useRangePickerValue from './hooks/useRangePickerValue'; import useRangeValue, { useInnerValue } from './hooks/useRangeValue'; import useShowNow from './hooks/useShowNow'; +import type { InvalidateDateInfo } from './hooks/useInvalidate'; import Popup, { type PopupShowTimeConfig } from './Popup'; import RangeSelector, { type SelectorIdType } from './Selector/RangeSelector'; import useSemantic from '../hooks/useSemantic'; @@ -332,6 +333,17 @@ function RangePicker( // ======================= Show Now ======================= const mergedShowNow = useShowNow(picker, mergedMode, showNow, showToday, true); + // ======================= Invalidate ====================== + const isRangeInvalidateDate = useEvent((date: DateType, info?: InvalidateDateInfo) => { + const infoActiveIndex = info?.activeIndex ?? activeIndex; + + return isInvalidateDate(date, { + ...(info || {}), + activeIndex: infoActiveIndex, + range: getActiveRange(infoActiveIndex), + }); + }); + // ======================== Value ========================= const [ /** Trigger `onChange` by check `disabledDate` */ @@ -348,13 +360,14 @@ function RangePicker( formatList, focused, mergedOpen, - isInvalidateDate, + isRangeInvalidateDate, ); // ===================== DisabledDate ===================== const mergedDisabledDate = useRangeDisabledDate( calendarValue, disabled, + activeIndex, activeIndexList, generateConfig, locale, @@ -364,7 +377,7 @@ function RangePicker( // ======================= Validate ======================= const [submitInvalidates, onSelectorInvalid] = useFieldsInvalidate( calendarValue, - isInvalidateDate, + isRangeInvalidateDate, allowEmpty, ); @@ -567,7 +580,7 @@ function RangePicker( // >>> invalid const isPopupInvalidateDate = useEvent((date: DateType) => { - return isInvalidateDate(date, { + return isRangeInvalidateDate(date, { activeIndex, }); }); diff --git a/src/PickerInput/hooks/useInvalidate.ts b/src/PickerInput/hooks/useInvalidate.ts index df907bb6a..2efee712a 100644 --- a/src/PickerInput/hooks/useInvalidate.ts +++ b/src/PickerInput/hooks/useInvalidate.ts @@ -1,12 +1,18 @@ import { useEvent } from '@rc-component/util'; import type { GenerateConfig } from '../../generate'; import type { + BaseInfo, PanelMode, RangeTimeProps, SharedPickerProps, SharedTimeProps, } from '../../interface'; +export type InvalidateDateInfo = BaseInfo & { + from?: DateType; + activeIndex: number; +}; + /** * Check if provided date is valid for the `disabledDate` & `showTime.disabledTime`. */ @@ -17,62 +23,60 @@ export default function useInvalidate( showTime?: SharedTimeProps | RangeTimeProps, ) { // Check disabled date - const isInvalidate = useEvent( - (date: DateType, info?: { from?: DateType; activeIndex: number }) => { - const outsideInfo = { type: picker, ...info }; - delete outsideInfo.activeIndex; + const isInvalidate = useEvent((date: DateType, info?: InvalidateDateInfo) => { + const outsideInfo = { type: picker, ...info }; + delete outsideInfo.activeIndex; - if ( - // Date object is invalid - !generateConfig.isValidate(date) || - // Date is disabled by `disabledDate` - (disabledDate && disabledDate(date, outsideInfo)) - ) { - return true; - } + if ( + // Date object is invalid + !generateConfig.isValidate(date) || + // Date is disabled by `disabledDate` + (disabledDate && disabledDate(date, outsideInfo)) + ) { + return true; + } - if ((picker === 'date' || picker === 'time') && showTime) { - const range = info && info.activeIndex === 1 ? 'end' : 'start'; - const { disabledHours, disabledMinutes, disabledSeconds, disabledMilliseconds } = - showTime.disabledTime?.(date, range, { from: outsideInfo.from }) || {}; + if ((picker === 'date' || picker === 'time') && showTime) { + const range = info && info.activeIndex === 1 ? 'end' : 'start'; + const { disabledHours, disabledMinutes, disabledSeconds, disabledMilliseconds } = + showTime.disabledTime?.(date, range, { from: outsideInfo.from }) || {}; - const { - disabledHours: legacyDisabledHours, - disabledMinutes: legacyDisabledMinutes, - disabledSeconds: legacyDisabledSeconds, - } = showTime; + const { + disabledHours: legacyDisabledHours, + disabledMinutes: legacyDisabledMinutes, + disabledSeconds: legacyDisabledSeconds, + } = showTime; - const mergedDisabledHours = disabledHours || legacyDisabledHours; - const mergedDisabledMinutes = disabledMinutes || legacyDisabledMinutes; - const mergedDisabledSeconds = disabledSeconds || legacyDisabledSeconds; + const mergedDisabledHours = disabledHours || legacyDisabledHours; + const mergedDisabledMinutes = disabledMinutes || legacyDisabledMinutes; + const mergedDisabledSeconds = disabledSeconds || legacyDisabledSeconds; - const hour = generateConfig.getHour(date); - const minute = generateConfig.getMinute(date); - const second = generateConfig.getSecond(date); - const millisecond = generateConfig.getMillisecond(date); + const hour = generateConfig.getHour(date); + const minute = generateConfig.getMinute(date); + const second = generateConfig.getSecond(date); + const millisecond = generateConfig.getMillisecond(date); - if (mergedDisabledHours && mergedDisabledHours().includes(hour)) { - return true; - } + if (mergedDisabledHours && mergedDisabledHours().includes(hour)) { + return true; + } - if (mergedDisabledMinutes && mergedDisabledMinutes(hour).includes(minute)) { - return true; - } + if (mergedDisabledMinutes && mergedDisabledMinutes(hour).includes(minute)) { + return true; + } - if (mergedDisabledSeconds && mergedDisabledSeconds(hour, minute).includes(second)) { - return true; - } + if (mergedDisabledSeconds && mergedDisabledSeconds(hour, minute).includes(second)) { + return true; + } - if ( - disabledMilliseconds && - disabledMilliseconds(hour, minute, second).includes(millisecond) - ) { - return true; - } + if ( + disabledMilliseconds && + disabledMilliseconds(hour, minute, second).includes(millisecond) + ) { + return true; } - return false; - }, - ); + } + return false; + }); return isInvalidate; } diff --git a/src/PickerInput/hooks/useRangeDisabledDate.ts b/src/PickerInput/hooks/useRangeDisabledDate.ts index 3aca5c1d8..1189ab706 100644 --- a/src/PickerInput/hooks/useRangeDisabledDate.ts +++ b/src/PickerInput/hooks/useRangeDisabledDate.ts @@ -11,25 +11,28 @@ import { getFromDate } from '../../utils/miscUtil'; export default function useRangeDisabledDate( values: RangeValueType, disabled: [boolean, boolean], + activeIndex: number, activeIndexList: number[], generateConfig: GenerateConfig, locale: Locale, disabledDate?: DisabledDate, ) { - const activeIndex = activeIndexList[activeIndexList.length - 1]; + const activeListIndex = activeIndexList[activeIndexList.length - 1]; const rangeDisabledDate: DisabledDate = (date, info) => { const [start, end] = values; + const range: 'start' | 'end' = activeIndex === 1 ? 'end' : 'start'; const mergedInfo = { ...info, from: getFromDate(values, activeIndexList), + range, }; // ============================ Disabled ============================ // Should not select days before the start date if ( - activeIndex === 1 && + activeListIndex === 1 && disabled[0] && start && // Same date isOK @@ -42,7 +45,7 @@ export default function useRangeDisabledDate( // Should not select days after the end date if ( - activeIndex === 0 && + activeListIndex === 0 && disabled[1] && end && // Same date isOK diff --git a/src/PickerInput/hooks/useRangeValue.ts b/src/PickerInput/hooks/useRangeValue.ts index 6cd0ac367..16a79c143 100644 --- a/src/PickerInput/hooks/useRangeValue.ts +++ b/src/PickerInput/hooks/useRangeValue.ts @@ -7,6 +7,7 @@ import { formatValue, isSame, isSameTimestamp } from '../../utils/dateUtil'; import { fillIndex } from '../../utils/miscUtil'; import type { RangePickerProps } from '../RangePicker'; import type { ReplacedPickerProps } from '../SinglePicker'; +import type { InvalidateDateInfo } from './useInvalidate'; import useLockEffect from './useLockEffect'; const EMPTY_VALUE: any[] = []; @@ -175,7 +176,7 @@ export default function useRangeValue boolean, + isInvalidateDate: (date: DateType, info?: InvalidateDateInfo) => boolean, ): [ /** Trigger `onChange` by check `disabledDate` */ flushSubmit: (index: number, needTriggerChange: boolean) => void, diff --git a/src/interface.tsx b/src/interface.tsx index 63a690d06..a11b6234c 100644 --- a/src/interface.tsx +++ b/src/interface.tsx @@ -89,9 +89,14 @@ export type InternalMode = PanelMode | 'datetime'; export type PickerMode = Exclude; +export interface BaseInfo { + /** Only work in RangePicker. Tell the related start or end field. */ + range?: 'start' | 'end'; +} + export type DisabledDate = ( date: DateType, - info: { + info: BaseInfo & { type: PanelMode; /** * Only work in RangePicker. @@ -102,10 +107,6 @@ export type DisabledDate = ( }, ) => boolean; -export interface BaseInfo { - range?: 'start' | 'end'; -} - export interface CellRenderInfo extends BaseInfo { prefixCls: string; // The cell wrapper element diff --git a/tests/new-range.spec.tsx b/tests/new-range.spec.tsx index 85a4dd07c..cb2d6f809 100644 --- a/tests/new-range.spec.tsx +++ b/tests/new-range.spec.tsx @@ -557,6 +557,29 @@ describe('NewPicker.Range', () => { expect(document.querySelector('.rc-picker-ok button')).toBeDisabled(); }); + it('disabledDate range should control ok button', () => { + const disabledDate = (date: Dayjs, info: { range?: 'start' | 'end' }) => + info.range === 'end' && date.isBefore(dayjs('2024-11-20'), 'day'); + + const { container } = render( + , + ); + + openPicker(container, 1); + expect(document.querySelector('.rc-picker-ok button')).not.toBeDisabled(); + + fireEvent.change(container.querySelectorAll('input')[1], { + target: { + value: '2024-11-19 00:00:00', + }, + }); + expect(document.querySelector('.rc-picker-ok button')).toBeDisabled(); + }); + it('disabledDate provides info.type', () => { const disabledDate = jest.fn(() => false); @@ -570,6 +593,72 @@ describe('NewPicker.Range', () => { ); }); + it('disabledDate provides info.range', () => { + const disabledDate = jest.fn(() => false); + + const { container } = render(); + + openPicker(container); + expect(disabledDate).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + range: 'start', + }), + ); + + disabledDate.mockClear(); + openPicker(container, 1); + expect(disabledDate).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + range: 'end', + }), + ); + }); + + it('disabledDate keeps from info behavior', () => { + const disabledDate = jest.fn( + (_date: Dayjs, _info: { range?: 'start' | 'end'; from?: Dayjs }) => false, + ); + + const { container } = render(); + + openPicker(container); + selectCell(15); + + const endCall = disabledDate.mock.calls.find(([, info]) => info.range === 'end' && info.from); + + expect(endCall).toBeTruthy(); + expect(isSame(endCall![1].from, '1990-09-15')).toBeTruthy(); + }); + + it('disabledDate can apply to end field only', () => { + const onChange = jest.fn(); + const disabledDate = (date: Dayjs, info: { range?: 'start' | 'end' }) => + info.range === 'end' && date <= dayjs('2024-11-20').endOf('day'); + + const { container } = render( + , + ); + + openPicker(container, 1); + + const disabledCell = selectCell('19', 1); + expect(disabledCell).toHaveClass('rc-picker-cell-disabled'); + expect(onChange).not.toHaveBeenCalled(); + + selectCell('21', 1); + expect(onChange).toHaveBeenCalledWith( + [expect.anything(), expect.anything()], + ['2024-10-28', '2024-11-21'], + ); + }); + it('disabled should patch className', () => { const { container, rerender } = render(); expect(container.querySelector('.rc-picker-disabled')).toBeTruthy();