Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/PickerInput/RangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -332,6 +333,17 @@ function RangePicker<DateType extends object = any>(
// ======================= Show Now =======================
const mergedShowNow = useShowNow(picker, mergedMode, showNow, showToday, true);

// ======================= Invalidate ======================
const isRangeInvalidateDate = useEvent((date: DateType, info?: InvalidateDateInfo<DateType>) => {
const infoActiveIndex = info?.activeIndex ?? activeIndex;

return isInvalidateDate(date, {
...(info || {}),
activeIndex: infoActiveIndex,
range: getActiveRange(infoActiveIndex),
});
Comment thread
QDyanbing marked this conversation as resolved.
});

// ======================== Value =========================
const [
/** Trigger `onChange` by check `disabledDate` */
Expand All @@ -348,13 +360,14 @@ function RangePicker<DateType extends object = any>(
formatList,
focused,
mergedOpen,
isInvalidateDate,
isRangeInvalidateDate,
);

// ===================== DisabledDate =====================
const mergedDisabledDate = useRangeDisabledDate(
calendarValue,
disabled,
activeIndex,
activeIndexList,
generateConfig,
locale,
Expand All @@ -364,7 +377,7 @@ function RangePicker<DateType extends object = any>(
// ======================= Validate =======================
const [submitInvalidates, onSelectorInvalid] = useFieldsInvalidate(
calendarValue,
isInvalidateDate,
isRangeInvalidateDate,
allowEmpty,
);

Expand Down Expand Up @@ -567,7 +580,7 @@ function RangePicker<DateType extends object = any>(

// >>> invalid
const isPopupInvalidateDate = useEvent((date: DateType) => {
return isInvalidateDate(date, {
return isRangeInvalidateDate(date, {
activeIndex,
});
});
Expand Down
96 changes: 50 additions & 46 deletions src/PickerInput/hooks/useInvalidate.ts
Original file line number Diff line number Diff line change
@@ -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<DateType = any> = BaseInfo & {
from?: DateType;
activeIndex: number;
};

/**
* Check if provided date is valid for the `disabledDate` & `showTime.disabledTime`.
*/
Expand All @@ -17,62 +23,60 @@ export default function useInvalidate<DateType extends object = any>(
showTime?: SharedTimeProps<DateType> | RangeTimeProps<DateType>,
) {
// 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<DateType>) => {
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;
}
9 changes: 6 additions & 3 deletions src/PickerInput/hooks/useRangeDisabledDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,28 @@ import { getFromDate } from '../../utils/miscUtil';
export default function useRangeDisabledDate<DateType extends object = any>(
values: RangeValueType<DateType>,
disabled: [boolean, boolean],
activeIndex: number,
activeIndexList: number[],
generateConfig: GenerateConfig<DateType>,
locale: Locale,
disabledDate?: DisabledDate<DateType>,
) {
const activeIndex = activeIndexList[activeIndexList.length - 1];
const activeListIndex = activeIndexList[activeIndexList.length - 1];

const rangeDisabledDate: DisabledDate<DateType> = (date, info) => {
const [start, end] = values;
const range: 'start' | 'end' = activeIndex === 1 ? 'end' : 'start';

const mergedInfo = {
...info,
from: getFromDate(values, activeIndexList),
range,
};
Comment thread
QDyanbing marked this conversation as resolved.

// ============================ Disabled ============================
// Should not select days before the start date
if (
activeIndex === 1 &&
activeListIndex === 1 &&
disabled[0] &&
start &&
// Same date isOK
Expand All @@ -42,7 +45,7 @@ export default function useRangeDisabledDate<DateType extends object = any>(

// Should not select days after the end date
if (
activeIndex === 0 &&
activeListIndex === 0 &&
disabled[1] &&
end &&
// Same date isOK
Expand Down
3 changes: 2 additions & 1 deletion src/PickerInput/hooks/useRangeValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -175,7 +176,7 @@ export default function useRangeValue<ValueType extends DateType[], DateType ext
formatList: FormatType[],
focused: boolean,
open: boolean,
isInvalidateDate: (date: DateType, info?: { from?: DateType; activeIndex: number }) => boolean,
isInvalidateDate: (date: DateType, info?: InvalidateDateInfo<DateType>) => boolean,
): [
/** Trigger `onChange` by check `disabledDate` */
flushSubmit: (index: number, needTriggerChange: boolean) => void,
Expand Down
11 changes: 6 additions & 5 deletions src/interface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,14 @@ export type InternalMode = PanelMode | 'datetime';

export type PickerMode = Exclude<PanelMode, 'datetime' | 'decade'>;

export interface BaseInfo {
/** Only work in RangePicker. Tell the related start or end field. */
range?: 'start' | 'end';
}

export type DisabledDate<DateType = any> = (
date: DateType,
info: {
info: BaseInfo & {
type: PanelMode;
/**
* Only work in RangePicker.
Expand All @@ -102,10 +107,6 @@ export type DisabledDate<DateType = any> = (
},
) => boolean;

export interface BaseInfo {
range?: 'start' | 'end';
}

export interface CellRenderInfo<DateType> extends BaseInfo {
prefixCls: string;
// The cell wrapper element
Expand Down
89 changes: 89 additions & 0 deletions tests/new-range.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DayRangePicker
showTime
defaultValue={[getDay('2024-11-19 00:00:00'), getDay('2024-11-21 00:00:00')]}
disabledDate={disabledDate}
/>,
);

openPicker(container, 1);
expect(document.querySelector('.rc-picker-ok button')).not.toBeDisabled();

fireEvent.change(container.querySelectorAll<HTMLInputElement>('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);

Expand All @@ -570,6 +593,72 @@ describe('NewPicker.Range', () => {
);
});

it('disabledDate provides info.range', () => {
const disabledDate = jest.fn(() => false);

const { container } = render(<DayRangePicker disabledDate={disabledDate} />);

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(<DayRangePicker disabledDate={disabledDate} />);

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(
<DayRangePicker
disabled={[true, false]}
defaultValue={[getDay('2024-10-28'), getDay('2024-11-20')]}
disabledDate={disabledDate}
onChange={onChange}
/>,
);

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(<DayRangePicker disabled />);
expect(container.querySelector('.rc-picker-disabled')).toBeTruthy();
Expand Down
Loading