Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: disabledTime validate user input #690

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
59 changes: 46 additions & 13 deletions src/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@
import type { AlignType } from '@rc-component/trigger/lib/interface';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import warning from 'rc-util/lib/warning';
import pickAttrs from 'rc-util/lib/pickAttrs';
import warning from 'rc-util/lib/warning';
import * as React from 'react';
import { GenerateConfig } from './generate';
import useHoverValue from './hooks/useHoverValue';
import usePickerInput from './hooks/usePickerInput';
import usePresets from './hooks/usePresets';
import useTextValueMapping from './hooks/useTextValueMapping';
import useValueTexts from './hooks/useValueTexts';
import type { CustomFormat, PickerMode, PresetDate } from './interface';
import type { CustomFormat, DisabledTime, PickerMode, PresetDate } from './interface';
import type { ContextOperationRefProps } from './PanelContext';
import PanelContext from './PanelContext';
import type {
Expand All @@ -34,10 +35,10 @@ import PickerPanel from './PickerPanel';
import PickerTrigger from './PickerTrigger';
import PresetPanel from './PresetPanel';
import { formatValue, isEqual, parseValue } from './utils/dateUtil';
import { getClearIcon } from './utils/getClearIcon';
import { toArray } from './utils/miscUtil';
import { elementsContains, getDefaultFormat, getInputSize } from './utils/uiUtil';
import { legacyPropsWarning } from './utils/warnUtil';
import { getClearIcon } from './utils/getClearIcon';

export type PickerRefConfig = {
focus: () => void;
Expand Down Expand Up @@ -67,8 +68,8 @@ export type PickerSharedProps<DateType> = {

// Render
suffixIcon?: React.ReactNode;
/**
* Clear all icon
/**
* Clear all icon
* @deprecated Please use `allowClear` instead
**/
clearIcon?: React.ReactNode;
Expand Down Expand Up @@ -145,6 +146,32 @@ type MergedPickerProps<DateType> = {
picker?: PickerMode;
} & OmitType<DateType>;

function testValueInSet(num: number, range: number[]) {
if (typeof num === 'undefined' || typeof range === 'undefined') return;
const set = new Set(range);
return set.has(num);
}

function validateTime<DateType>(
picker: PickerMode,
disabledTime: DisabledTime<DateType>,
date: DateType,
generateConfig: GenerateConfig<DateType>,
) {
if (!disabledTime || picker !== 'date') return false;
const disabledTimes = disabledTime(date);
if (!disabledTimes) return false;
const { disabledHours, disabledMinutes, disabledSeconds } = disabledTimes;
const hour = generateConfig.getHour(date);
const minter = generateConfig.getMinute(date);
const second = generateConfig.getSecond(date);

const validateHour = testValueInSet(hour, disabledHours?.());
const validateMinute = testValueInSet(minter, disabledMinutes?.(hour));
const validateSecond = testValueInSet(second, disabledSeconds?.(hour, minter));
return validateHour || validateMinute || validateSecond;
}

function InnerPicker<DateType>(props: PickerProps<DateType>) {
const {
prefixCls = 'rc-picker',
Expand Down Expand Up @@ -196,6 +223,7 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
autoComplete = 'off',
inputRender,
changeOnBlur,
disabledTime,
} = props as MergedPickerProps<DateType>;

const inputRef = React.useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -253,6 +281,8 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
locale,
});

const timeProps = typeof showTime === 'object' ? showTime : {};

const [text, triggerTextChange, resetText] = useTextValueMapping({
valueTexts,
onTextChange: (newText) => {
Expand All @@ -261,7 +291,11 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
formatList,
generateConfig,
});
if (inputDate && (!disabledDate || !disabledDate(inputDate))) {
if (
inputDate &&
(!disabledDate || !disabledDate(inputDate)) &&
!validateTime(picker, disabledTime, inputDate || timeProps?.defaultValue, generateConfig)
) {
setSelectedValue(inputDate);
}
},
Expand Down Expand Up @@ -340,7 +374,8 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
// When user typing disabledDate with keyboard and enter, this value will be empty
!selectedValue ||
// Normal disabled check
(disabledDate && disabledDate(selectedValue))
(disabledDate && disabledDate(selectedValue)) ||
validateTime(picker, disabledTime, selectedValue, generateConfig)
) {
return false;
}
Expand Down Expand Up @@ -490,11 +525,7 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {
);
}

const mergedClearIcon: React.ReactNode = getClearIcon(
prefixCls,
allowClear,
clearIcon,
);
const mergedClearIcon: React.ReactNode = getClearIcon(prefixCls, allowClear, clearIcon);

const clearNode: React.ReactNode = (
<span
Expand All @@ -517,7 +548,9 @@ function InnerPicker<DateType>(props: PickerProps<DateType>) {

const mergedAllowClear = !!allowClear && mergedValue && !disabled;

const mergedInputProps: React.InputHTMLAttributes<HTMLInputElement> & { ref: React.MutableRefObject<HTMLInputElement> } = {
const mergedInputProps: React.InputHTMLAttributes<HTMLInputElement> & {
ref: React.MutableRefObject<HTMLInputElement>;
} = {
id,
tabIndex,
disabled,
Expand Down
95 changes: 91 additions & 4 deletions tests/disabledTime.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ import {
openPicker,
} from './util/commonUtil';

import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import KeyCode from 'rc-util/lib/KeyCode';
dayjs.extend(customParseFormat);

function keyDown(keyCode: number) {
fireEvent.keyDown(document.querySelector('input'), {
keyCode,
which: keyCode,
charCode: keyCode,
});
}

describe('Picker.DisabledTime', () => {
it('disabledTime on TimePicker', () => {
render(
Expand Down Expand Up @@ -153,11 +166,15 @@ describe('Picker.DisabledTime', () => {
/>,
);

expect(document.querySelector('.rc-picker-input > input').getAttribute('value')).toEqual('1989-11-28 00:00:00');
expect(document.querySelector('.rc-picker-input > input').getAttribute('value')).toEqual(
'1989-11-28 00:00:00',
);

fireEvent.click(document.querySelectorAll('.rc-picker-cell-inner')[2]);

expect(document.querySelector('.rc-picker-input > input').getAttribute('value')).toEqual('1989-10-31 05:00:00');
expect(document.querySelector('.rc-picker-input > input').getAttribute('value')).toEqual(
'1989-10-31 05:00:00',
);
});

it('disabledTime should reset correctly when date changed by click for no default value', function () {
Expand All @@ -174,8 +191,12 @@ describe('Picker.DisabledTime', () => {

const firstDayInMonth = now.startOf('month');
const firstDayInCalendar = firstDayInMonth.clone().subtract(firstDayInMonth.days(), 'days');
const expected = firstDayInCalendar.clone().hour(h + 1 % 24).minute(m + 1 % 60).second(s + 1 % 60);

const expected = firstDayInCalendar
.clone()
.hour(h + (1 % 24))
.minute(m + (1 % 60))
.second(s + (1 % 60));

render(<MomentRangePicker open showTime disabledTime={disabledTime} />);

fireEvent.click(document.querySelectorAll('.rc-picker-cell-inner')[0]);
Expand All @@ -184,6 +205,72 @@ describe('Picker.DisabledTime', () => {
expected.format('YYYY-MM-DD HH:mm:ss'),
);
});
// https://github.com/ant-design/ant-design/issues/45489
it('disabledTime should reset correctly when date time change by input enter', () => {
resetWarned();
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const range = (start, end) => {
const result = [];
for (let i = start; i < end; i++) {
result.push(i);
}
return result;
};
const disabledDate = (current) => {
return current && current.isBefore(Date.now());
};
const disabledDateTime = () => ({
disabledHours: () => range(0, 24).splice(4, 20),
disabledMinutes: () => range(30, 60),
disabledSeconds: () => [55, 56],
});

const expectDate = '2023-10-26 00:00:00';
const changingDate = '2023-10-26 03:00:00';
const nowAllowDate = '2023-10-26 06:00:00';

const { container } = render(
<MomentPicker
showTime
picker="date"
format="YYYY-MM-DD HH:mm:ss"
disabledDate={disabledDate}
disabledTime={disabledDateTime}
defaultValue={moment(expectDate)}
/>,
);
openPicker(container);
const yearPanel = document.querySelector('.rc-picker-time-panel-column');
expect(yearPanel.querySelectorAll('li')[4]).toHaveClass('rc-picker-time-panel-cell-disabled');
closePicker(container);

expect(document.querySelector('.rc-picker-input input').getAttribute('value')).toEqual(
expectDate,
);

fireEvent.click(document.querySelector('.rc-picker-input > input'));
fireEvent.change(document.querySelector('.rc-picker-input > input'), {
target: {
value: changingDate,
},
});
keyDown(KeyCode.ENTER);
expect(document.querySelector('.rc-picker-input input').getAttribute('value')).toEqual(
changingDate,
);
fireEvent.click(document.querySelector('.rc-picker-input > input'));
fireEvent.change(document.querySelector('.rc-picker-input > input'), {
target: {
value: nowAllowDate,
},
});

keyDown(KeyCode.ENTER);
expect(document.querySelector('.rc-picker-input input').getAttribute('value')).toEqual(
changingDate,
);
errSpy.mockRestore();
});

describe('warning for legacy props', () => {
it('single', () => {
Expand Down
Loading