Skip to content

Commit

Permalink
feat(datepicker): support keyboard input on DatePicker (#1350)
Browse files Browse the repository at this point in the history
  • Loading branch information
simonguo committed Nov 19, 2020
1 parent ced7670 commit 36e3268
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 135 deletions.
99 changes: 88 additions & 11 deletions src/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import mapValues from 'lodash/mapValues';
import pick from 'lodash/pick';
import omit from 'lodash/omit';
import { Calendar, CalendarLocale, CalendarState } from '../Calendar';
import { Calendar, CalendarState } from '../Calendar';
import Toolbar, { RangeType } from './Toolbar';
import { DatePickerLocale } from '../locales';

import {
composeFunctions,
Expand Down Expand Up @@ -44,7 +45,7 @@ import { useCalendarState } from './utils';
export type { RangeType } from './Toolbar';

export interface DatePickerProps
extends PickerBaseProps<CalendarLocale>,
extends PickerBaseProps<DatePickerLocale>,
FormControlBaseProps<Date> {
/** Configure shortcut options */
ranges?: RangeType[];
Expand Down Expand Up @@ -122,7 +123,7 @@ export interface DatePickerProps
onClean?: (event: React.MouseEvent) => void;

/** Custom render value */
renderValue?: (value: Date, format: string) => React.ReactNode;
renderValue?: (value: Date, format: string) => string;
}

const defaultProps: Partial<DatePickerProps> = {
Expand All @@ -134,6 +135,8 @@ const defaultProps: Partial<DatePickerProps> = {
placeholder: ''
};

type InputState = 'Typing' | 'Error' | 'Initial';

const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwardRef(
(props: DatePickerProps, ref) => {
const {
Expand Down Expand Up @@ -178,7 +181,7 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
...rest
} = props;

const { locale, formatDate } = useCustom<CalendarLocale>('DatePicker', overrideLocale);
const { locale, formatDate } = useCustom<DatePickerLocale>('DatePicker', overrideLocale);
const { merge, prefix, withClassPrefix } = useClassNames(classPrefix);

// Format the value according to the time zone.
Expand All @@ -190,6 +193,8 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
TimeZone.toTimeZone(value ?? calendarDefaultDate ?? new Date(), timeZone)
);

const [inputState, setInputState] = useState<InputState>();

const { calendarState, reset, openMonth, openTime } = useCalendarState();
const [active, setActive] = useState<boolean>(false);
const triggerRef = useRef<OverlayTriggerInstance>();
Expand Down Expand Up @@ -311,6 +316,9 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
[handleDateChange, updateValue]
);

/**
* The callback triggered after clicking the OK button.
*/
const handleOK = useCallback(
(event: React.SyntheticEvent<any>) => {
updateValue(event);
Expand All @@ -319,6 +327,9 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
[getLocalPageDate, updateValue, onOk]
);

/**
* Toggle month selection panel
*/
const handleMonthDropdown = useCallback(() => {
if (calendarState === CalendarState.DROP_MONTH) {
reset();
Expand All @@ -329,6 +340,9 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
onToggleMonthDropdown?.(calendarState !== CalendarState.DROP_MONTH);
}, [calendarState, onToggleMonthDropdown, openMonth, reset]);

/**
* Switch time selection panel
*/
const handleTimeDropdown = useCallback(() => {
if (calendarState === CalendarState.DROP_TIME) {
reset();
Expand All @@ -339,6 +353,9 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
onToggleTimeDropdown?.(calendarState !== CalendarState.DROP_TIME);
}, [calendarState, onToggleTimeDropdown, openTime, reset]);

/**
* Callback after clicking the clear button.
*/
const handleClean = useCallback(
(event: React.SyntheticEvent) => {
setPageDate(TimeZone.toTimeZone(new Date(), timeZone));
Expand All @@ -347,6 +364,9 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
[updateValue, timeZone]
);

/**
* Handle keyboard events.
*/
const onPickerKeyDown = useToggleKeyDownEvent({
triggerRef,
toggleRef,
Expand All @@ -355,8 +375,11 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
...rest
});

/**
* Callback after the date is selected.
*/
const handleSelect = useCallback(
(nextValue: Date, event: React.SyntheticEvent) => {
(nextValue: Date, event: React.SyntheticEvent, updatableValue = true) => {
setPageDate(
composeFunctions(
(d: Date) => DateUtils.setHours(d, DateUtils.getHours(pageDate)),
Expand All @@ -366,11 +389,61 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
);

handleDateChange(nextValue);
oneTap && updateValue(event, nextValue);
if (oneTap && updatableValue) {
updateValue(event, nextValue);
}
},
[handleDateChange, updateValue, oneTap, pageDate]
);

const disabledDate = useCallback(
(date?: Date) => disabledDateProp?.(TimeZone.toLocalTimeZone(date, timeZone)),
[disabledDateProp, timeZone]
);

/**
* Callback after the input box value is changed.
*/
const handleInputChange = useCallback(
(value, event) => {
setInputState('Typing');

// isMatch('01/11/2020', 'MM/dd/yyyy') ==> true
// isMatch('2020-11-01', 'MM/dd/yyyy') ==> false
if (!DateUtils.isMatch(value, formatStr)) {
setInputState('Error');
return;
}
const date = new Date(value);

if (!DateUtils.isValid(date)) {
setInputState('Error');
return;
}

if (disabledDate(date)) {
setInputState('Error');
return;
}

handleSelect(date, event, false);
},
[disabledDate, formatStr, handleSelect]
);

/**
* The callback after the enter key is triggered on the input
*/
const handleInputBlur = useCallback(
event => {
if (inputState === 'Typing') {
updateValue(event, pageDate);
}
setInputState('Initial');
},
[inputState, pageDate, updateValue]
);

const handleEntered = useCallback(() => {
onOpen?.();
setActive(true);
Expand All @@ -382,11 +455,6 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
setActive(false);
}, [onClose, reset]);

const disabledDate = useCallback(
(date?: Date) => disabledDateProp?.(TimeZone.toLocalTimeZone(date, timeZone)),
[disabledDateProp, timeZone]
);

// Check whether the time is within the time range of the shortcut option in the toolbar.
const disabledToolbarHandle = useCallback(
(date?: Date): boolean => {
Expand Down Expand Up @@ -489,6 +557,7 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa

return (
<PickerToggleTrigger
trigger="active"
pickerProps={pick(props, pickTriggerPropKeys)}
ref={triggerRef}
placement={placement}
Expand All @@ -511,8 +580,16 @@ const DatePicker: RsRefForwardingComponent<'div', DatePickerProps> = React.forwa
...usedClassNamePropKeys,
...DateUtils.calendarOnlyProps
])}
className={prefix({ error: inputState === 'Error' })}
as={toggleAs}
ref={toggleRef}
input
inputValue={value ? formatDate(value, formatStr) : ''}
inputPlaceholder={
typeof placeholder === 'string' && placeholder ? placeholder : formatStr
}
onInputChange={handleInputChange}
onInputBlur={handleInputBlur}
onKeyDown={onPickerKeyDown}
onClean={createChainedFunction(handleClean, onClean)}
cleanable={cleanable && !disabled}
Expand Down
4 changes: 2 additions & 2 deletions src/DatePicker/test/DatePickerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@ describe('DatePicker ', () => {
done();
};

const instance = getDOMNode(<DatePicker onClose={doneOp} defaultOpen />);
ReactTestUtils.Simulate.click(instance.querySelector('[role="combobox"]'));
const instance = getInstance(<DatePicker onClose={doneOp} defaultOpen />);
ReactTestUtils.Simulate.click(instance.menu.querySelector('.rs-picker-toolbar-right-btn-ok'));
});

it('Should not change for the value when it is controlled', done => {
Expand Down
3 changes: 2 additions & 1 deletion src/DateRangePicker/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ import {
} from '../Picker';
import { toLocalTimeZone } from '../utils/timeZone';
import { FormControlBaseProps, PickerBaseProps, TimeZoneName } from '../@types/common';
import { DateRangePickerLocale, DisabledDateFunction, RangeType, ValueType } from './types';
import { DisabledDateFunction, RangeType, ValueType } from './types';
import partial from 'lodash/partial';
import useUpdateEffect from '../utils/useUpdateEffect';
import { DateRangePickerLocale } from '../locales';

export interface DateRangePickerProps extends PickerBaseProps, FormControlBaseProps<ValueType> {
/** Configure shortcut options */
Expand Down
5 changes: 0 additions & 5 deletions src/DateRangePicker/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { CalendarLocale } from '../Calendar/types';

export type ValueType = [Date?, Date?];

Expand Down Expand Up @@ -28,7 +27,3 @@ export type DisabledDateFunction = (
// Call the target of the `disabledDate` function
target?: DATE_RANGE_DISABLED_TARGET
) => boolean;

export interface DateRangePickerLocale extends CalendarLocale {
last7Days?: string;
}
Loading

0 comments on commit 36e3268

Please sign in to comment.