diff --git a/src/PickerInput/RangePicker.tsx b/src/PickerInput/RangePicker.tsx index 36c2dda2e..476884aad 100644 --- a/src/PickerInput/RangePicker.tsx +++ b/src/PickerInput/RangePicker.tsx @@ -39,6 +39,7 @@ import useRangePickerValue from './hooks/useRangePickerValue'; import useRangeValue, { useInnerValue } from './hooks/useRangeValue'; import useRangeValueChange, { type RangeValueChangeSource } from './hooks/useRangeValueChange'; import useShowNow from './hooks/useShowNow'; +import type { InvalidateDateInfo } from './hooks/useInvalidate'; import Popup, { type PopupShowTimeConfig } from './Popup'; import RangeSelector, { type RangeSelectorRef, @@ -292,6 +293,17 @@ function RangePicker( }, ); + // ======================= 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` */ @@ -308,7 +320,7 @@ function RangePicker( triggerCalendarChange, disabled, formatList, - isInvalidateDate, + isRangeInvalidateDate, ); const triggerFieldCalendarChange = useEvent((index: number, date: DateType) => { @@ -407,7 +419,7 @@ function RangePicker( // ======================= Validate ======================= const [submitInvalidates, onSelectorInvalid] = useFieldsInvalidate( calendarValue, - isInvalidateDate, + isRangeInvalidateDate, allowEmpty, ); @@ -585,7 +597,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 f897010bc..ca67ceb62 100644 --- a/src/PickerInput/hooks/useRangeDisabledDate.ts +++ b/src/PickerInput/hooks/useRangeDisabledDate.ts @@ -19,10 +19,12 @@ export default function useRangeDisabledDate( ) { const rangeDisabledDate: DisabledDate = (date, info) => { const [start, end] = values; + const range: 'start' | 'end' = activeIndex === 1 ? 'end' : 'start'; const mergedInfo = { ...info, from: getFromDate(values, triggeredFields, activeIndex), + range, }; // ============================ Disabled ============================ diff --git a/src/PickerInput/hooks/useRangeValue.ts b/src/PickerInput/hooks/useRangeValue.ts index 5de85f5c3..1c8cbb514 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'; const EMPTY_VALUE: any[] = []; @@ -172,7 +173,7 @@ export default function useRangeValue, disabled: ReplaceListType, boolean>, formatList: FormatType[], - isInvalidateDate: (date: DateType, info?: { from?: DateType; activeIndex: number }) => boolean, + isInvalidateDate: (date: DateType, info?: InvalidateDateInfo) => boolean, ): [ /** Trigger `onChange` by check `disabledDate` */ flushSubmit: (index: number, needTriggerChange: boolean) => void, diff --git a/src/generate/dateFns.ts b/src/generate/dateFns.ts index c1c97d91c..50ef642ae 100644 --- a/src/generate/dateFns.ts +++ b/src/generate/dateFns.ts @@ -49,7 +49,7 @@ const localeParse = (format: string) => { const parse = (text: string, format: string, locale: string) => { return parseDate(text, localeParse(format), new Date(), { locale: getLocale(locale) }); -} +}; /** * Check if the text is a valid date considering the format and locale @@ -69,7 +69,7 @@ const isStrictValidDate = (text: string, format: string, locale: string) => { } const formattedDate = formatDate(date, format, { locale: getLocale(locale) }); return text === formattedDate; -} +}; const generateConfig: GenerateConfig = { // get 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 24cfbe0a5..c278b43b6 100644 --- a/tests/new-range.spec.tsx +++ b/tests/new-range.spec.tsx @@ -563,6 +563,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); @@ -576,6 +599,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();