From efd9556615fcb17f97e3f5d8f0f923771cf5731b Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Thu, 7 May 2020 18:29:54 +0300 Subject: [PATCH] [DateRangePicker] Fix when typed invalid value, then closed and reopened the DateRangePicker(#1755) * [DateRangePicker] Fix crashing when invalid date input applied as valiue * Do not run `findClosestEnabledDate` for DateRangePicker calendars * Remove inline style from , closes #1679 * [DateRangePicker] Keep input values even if they are invalid * Fix missing import * [DateRangePicker] Make impossible to navigate to the date after min/max date from calendar * Fix issues found by ts --- docs/prop-types.json | 69 ------------------- lib/.size-snapshot.json | 22 +++--- .../DateRangePicker/DateRangePickerInput.tsx | 25 +++---- .../DateRangePickerViewDesktop.tsx | 20 ++++-- .../DateRangePickerViewMobile.tsx | 15 +++- lib/src/__tests__/DateRangePicker.test.tsx | 34 +++++++-- lib/src/__tests__/test-utils.tsx | 15 ++-- lib/src/_helpers/date-utils.ts | 2 +- lib/src/_shared/PureDateInput.tsx | 2 - lib/src/_shared/hooks/useMaskedInput.tsx | 1 - lib/src/_shared/hooks/usePickerState.ts | 3 +- lib/src/views/Calendar/Calendar.tsx | 31 ++------- lib/src/views/Calendar/CalendarView.tsx | 22 +++++- 13 files changed, 118 insertions(+), 143 deletions(-) diff --git a/docs/prop-types.json b/docs/prop-types.json index 75d24e1d9..c6fa06773 100644 --- a/docs/prop-types.json +++ b/docs/prop-types.json @@ -3804,32 +3804,6 @@ } }, "Calendar": { - "minDate": { - "defaultValue": null, - "description": "Min selectable date", - "name": "minDate", - "parent": { - "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", - "name": "CalendarProps" - }, - "required": true, - "type": { - "name": "any" - } - }, - "maxDate": { - "defaultValue": null, - "description": "Max selectable date", - "name": "maxDate", - "parent": { - "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", - "name": "CalendarProps" - }, - "required": true, - "type": { - "name": "any" - } - }, "onChange": { "defaultValue": null, "description": "Calendar onChange", @@ -3913,49 +3887,6 @@ "type": { "name": "boolean" } - }, - "shouldDisableDate": { - "defaultValue": null, - "description": "Disable specific date", - "name": "shouldDisableDate", - "parent": { - "fileName": "material-ui-pickers/lib/src/_helpers/date-utils.ts", - "name": "DateValidationProps" - }, - "required": false, - "type": { - "name": "(day: DateIOType) => boolean" - } - }, - "disablePast": { - "defaultValue": { - "value": "false" - }, - "description": "Disable past dates", - "name": "disablePast", - "parent": { - "fileName": "material-ui-pickers/lib/src/_helpers/date-utils.ts", - "name": "DateValidationProps" - }, - "required": false, - "type": { - "name": "boolean" - } - }, - "disableFuture": { - "defaultValue": { - "value": "false" - }, - "description": "Disable future dates", - "name": "disableFuture", - "parent": { - "fileName": "material-ui-pickers/lib/src/_helpers/date-utils.ts", - "name": "DateValidationProps" - }, - "required": false, - "type": { - "name": "boolean" - } } }, "Day": { diff --git a/lib/.size-snapshot.json b/lib/.size-snapshot.json index e49249a63..8d9e7fb85 100644 --- a/lib/.size-snapshot.json +++ b/lib/.size-snapshot.json @@ -1,26 +1,26 @@ { "build/dist/material-ui-pickers.esm.js": { - "bundled": 188885, - "minified": 101313, - "gzipped": 26393, + "bundled": 188938, + "minified": 101335, + "gzipped": 26402, "treeshaked": { "rollup": { - "code": 83394, + "code": 83375, "import_statements": 2121 }, "webpack": { - "code": 92872 + "code": 92851 } } }, "build/dist/material-ui-pickers.umd.js": { - "bundled": 299785, - "minified": 116974, - "gzipped": 33364 + "bundled": 299832, + "minified": 116954, + "gzipped": 33374 }, "build/dist/material-ui-pickers.umd.min.js": { - "bundled": 258439, - "minified": 107835, - "gzipped": 30612 + "bundled": 258486, + "minified": 107815, + "gzipped": 30601 } } diff --git a/lib/src/DateRangePicker/DateRangePickerInput.tsx b/lib/src/DateRangePicker/DateRangePickerInput.tsx index b38bd96fe..41b7d22da 100644 --- a/lib/src/DateRangePicker/DateRangePickerInput.tsx +++ b/lib/src/DateRangePicker/DateRangePickerInput.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { RangeInput, DateRange } from './RangeTypes'; +import { useUtils } from '../_shared/hooks/useUtils'; import { makeStyles } from '@material-ui/core/styles'; import { MaterialUiPickersDate } from '../typings/date'; import { CurrentlySelectingRangeEndProps } from './RangeTypes'; @@ -62,26 +63,26 @@ export interface DateRangeInputProps } export const DateRangePickerInput: React.FC = ({ - rawValue, - onChange, - rawValue: [start, end], - parsedDateValue: [parsedStart, parsedEnd], - open, containerRef, - forwardedRef, currentlySelectingRangeEnd, - setCurrentlySelectingRangeEnd, - openPicker, disableOpenPicker, - startText, endText, + forwardedRef, + onBlur, + onChange, + open, + openPicker, + rawValue, readOnly, renderInput, + setCurrentlySelectingRangeEnd, + startText, TextFieldProps, - onBlur, + rawValue: [start, end], validationError: [startValidationError, endValidationError], ...other }) => { + const utils = useUtils(); const classes = useStyles(); const startRef = React.useRef(null); const endRef = React.useRef(null); @@ -108,11 +109,11 @@ export const DateRangePickerInput: React.FC = ({ ); const handleStartChange = (date: MaterialUiPickersDate, inputString?: string) => { - lazyHandleChangeCallback([date, parsedEnd], inputString); + lazyHandleChangeCallback([date, utils.date(end)], inputString); }; const handleEndChange = (date: MaterialUiPickersDate, inputString?: string) => { - lazyHandleChangeCallback([parsedStart, date], inputString); + lazyHandleChangeCallback([utils.date(start), date], inputString); }; const openRangeStartSelection = () => { diff --git a/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx b/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx index f3e0da160..a5a5d77fa 100644 --- a/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx +++ b/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx @@ -6,12 +6,18 @@ import { makeStyles } from '@material-ui/core/styles'; import { MaterialUiPickersDate } from '../typings/date'; import { calculateRangePreview } from './date-range-manager'; import { Calendar, CalendarProps } from '../views/Calendar/Calendar'; -import { isWithinRange, isStartOfRange, isEndOfRange } from '../_helpers/date-utils'; +import { defaultMinDate, defaultMaxDate } from '../constants/prop-types'; import { ArrowSwitcher, ExportedArrowSwitcherProps } from '../_shared/ArrowSwitcher'; import { usePreviousMonthDisabled, useNextMonthDisabled, } from '../_shared/hooks/date-helpers-hooks'; +import { + isWithinRange, + isStartOfRange, + isEndOfRange, + DateValidationProps, +} from '../_helpers/date-utils'; export interface ExportedDesktopDateRangeCalendarProps { /** @@ -24,6 +30,7 @@ export interface ExportedDesktopDateRangeCalendarProps { interface DesktopDateRangeCalendarProps extends ExportedDesktopDateRangeCalendarProps, CalendarProps, + DateValidationProps, ExportedArrowSwitcherProps { date: DateRange; changeMonth: (date: MaterialUiPickersDate) => void; @@ -82,14 +89,17 @@ export const DateRangePickerViewDesktop: React.FC onChange, disableFuture, disablePast, - minDate, - maxDate, + minDate: __minDate, + maxDate: __maxDate, currentlySelectingRangeEnd, currentMonth, ...other }) => { const utils = useUtils(); const classes = useStyles(); + const minDate = __minDate || utils.date(defaultMinDate); + const maxDate = __maxDate || utils.date(defaultMaxDate); + const [rangePreviewDay, setRangePreviewDay] = React.useState(null); const isNextMonthDisabled = useNextMonthDisabled(currentMonth, { disableFuture, maxDate }); @@ -161,10 +171,6 @@ export const DateRangePickerViewDesktop: React.FC {...other} key={index} date={date} - minDate={minDate} - maxDate={maxDate} - disablePast={disablePast} - disableFuture={disableFuture} className={classes.calendar} onChange={handleDayChange} currentMonth={monthOnIteration} diff --git a/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx b/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx index ca6da5cf9..e40eddb01 100644 --- a/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx +++ b/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx @@ -6,13 +6,20 @@ import { useUtils } from '../_shared/hooks/useUtils'; import { MaterialUiPickersDate } from '../typings/date'; import { Calendar, CalendarProps } from '../views/Calendar/Calendar'; import { ExportedArrowSwitcherProps } from '../_shared/ArrowSwitcher'; -import { isWithinRange, isStartOfRange, isEndOfRange } from '../_helpers/date-utils'; +import { defaultMinDate, defaultMaxDate } from '../constants/prop-types'; +import { + isWithinRange, + isStartOfRange, + isEndOfRange, + DateValidationProps, +} from '../_helpers/date-utils'; export interface ExportedMobileDateRangeCalendarProps {} interface DesktopDateRangeCalendarProps extends ExportedMobileDateRangeCalendarProps, CalendarProps, + DateValidationProps, ExportedArrowSwitcherProps { date: DateRange; changeMonth: (date: MaterialUiPickersDate) => void; @@ -30,9 +37,13 @@ export const DateRangePickerViewMobile: React.FC rightArrowButtonText, rightArrowIcon, onChange, + minDate: __minDate, + maxDate: __maxDate, ...other }) => { const utils = useUtils(); + const minDate = __minDate || utils.date(defaultMinDate); + const maxDate = __maxDate || utils.date(defaultMaxDate); return ( <> @@ -47,6 +58,8 @@ export const DateRangePickerViewMobile: React.FC rightArrowButtonProps={rightArrowButtonProps} rightArrowButtonText={rightArrowButtonText} rightArrowIcon={rightArrowIcon} + minDate={minDate} + maxDate={maxDate} {...other} /> diff --git a/lib/src/__tests__/DateRangePicker.test.tsx b/lib/src/__tests__/DateRangePicker.test.tsx index eb963e3ad..cc909d265 100644 --- a/lib/src/__tests__/DateRangePicker.test.tsx +++ b/lib/src/__tests__/DateRangePicker.test.tsx @@ -1,16 +1,23 @@ // Note that most of use cases are covered in cypress tests e2e/integration/DateRange.spec.ts import * as React from 'react'; import { isWeekend } from 'date-fns'; -import { TextField } from '@material-ui/core'; import { mount, utilsToUse } from './test-utils'; +import { TextField, TextFieldProps } from '@material-ui/core'; import { DesktopDateRangePicker } from '../DateRangePicker/DateRangePicker'; +const defaultRangeRenderInput = (startProps: TextFieldProps, endProps: TextFieldProps) => ( + <> + + + +); + describe('DateRangePicker', () => { test('allows select range', () => { const component = mount( } open + renderInput={defaultRangeRenderInput} onChange={jest.fn()} value={[ utilsToUse.date(new Date('2018-01-01T00:00:00.000Z')), @@ -25,8 +32,8 @@ describe('DateRangePicker', () => { test('allows disabling dates', () => { const component = mount( } open + renderInput={defaultRangeRenderInput} minDate={new Date('2005-01-01')} shouldDisableDate={date => isWeekend(utilsToUse.toJsDate(date))} onChange={jest.fn()} @@ -47,8 +54,8 @@ describe('DateRangePicker', () => { test('prop: calendars', () => { const component = mount( } open + renderInput={defaultRangeRenderInput} calendars={3} onChange={jest.fn()} value={[ @@ -61,4 +68,23 @@ describe('DateRangePicker', () => { expect(component.find('Calendar').length).toBe(3); expect(component.find('button[data-mui-test="DateRangeDay"]').length).toBe(90); }); + + test(`doesn't crashes if opening picker with invalid date input`, () => { + const component = mount( + + ); + + component + .find('input') + .at(1) + .simulate('focus'); + + expect(component.find('div[role="tooltip"]').length).toBe(1); + }); }); diff --git a/lib/src/__tests__/test-utils.tsx b/lib/src/__tests__/test-utils.tsx index 893a923ce..eb0b49539 100644 --- a/lib/src/__tests__/test-utils.tsx +++ b/lib/src/__tests__/test-utils.tsx @@ -8,6 +8,7 @@ import LocalizationProvider from '../LocalizationProvider'; import { IUtils } from '@date-io/core/IUtils'; import { DatePickerProps } from '../DatePicker'; import { MaterialUiPickersDate } from '../typings/date'; +import { BasePickerProps } from '../typings/BasePicker'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; interface WithUtilsProps { @@ -43,10 +44,14 @@ export const mount =

(element: React.ReactElement

) ); -export const mountPickerWithState = ( - defaultValue: MaterialUiPickersDate, - render: (props: Pick) => React.ReactElement -) => { +export function mountPickerWithState( + defaultValue: TValue, + render: ( + props: Pick, 'onChange' | 'value'> & { + renderInput: DatePickerProps['renderInput']; + } + ) => React.ReactElement +) { const PickerMountComponent = () => { const [value, setDate] = React.useState(defaultValue); @@ -58,7 +63,7 @@ export const mountPickerWithState = ( }; return mount(); -}; +} export const shallowRender = (render: (props: any) => React.ReactElement) => { return enzyme.shallow(render({ utils: utilsToUse, classes: {} as any, theme: {} as any })); diff --git a/lib/src/_helpers/date-utils.ts b/lib/src/_helpers/date-utils.ts index 790952751..709a79a49 100644 --- a/lib/src/_helpers/date-utils.ts +++ b/lib/src/_helpers/date-utils.ts @@ -115,7 +115,7 @@ export function parseRangeInputValue( { value = [null, null] }: BasePickerProps ) { return value.map(date => - date === null ? null : utils.startOfDay(utils.date(date)) + !utils.isValid(date) || date === null ? null : utils.startOfDay(utils.date(date)) ) as DateRange; } diff --git a/lib/src/_shared/PureDateInput.tsx b/lib/src/_shared/PureDateInput.tsx index 3ad0de376..005ecbbdb 100644 --- a/lib/src/_shared/PureDateInput.tsx +++ b/lib/src/_shared/PureDateInput.tsx @@ -13,7 +13,6 @@ export type MuiTextFieldProps = TextFieldProps | Omit export interface DateInputProps { open: boolean; rawValue: TInputValue; - parsedDateValue: TDateValue; inputFormat: string; onChange: (date: TDateValue, keyboardInputValue?: string) => void; openPicker: () => void; @@ -83,7 +82,6 @@ export type ExportedDateInputProps = Omit< | 'validationError' | 'rawValue' | 'forwardedRef' - | 'parsedDateValue' | 'open' | 'TextFieldProps' | 'onBlur' diff --git a/lib/src/_shared/hooks/useMaskedInput.tsx b/lib/src/_shared/hooks/useMaskedInput.tsx index 73172c8f7..939db0261 100644 --- a/lib/src/_shared/hooks/useMaskedInput.tsx +++ b/lib/src/_shared/hooks/useMaskedInput.tsx @@ -21,7 +21,6 @@ type MaskedInputProps = Omit< | 'disableOpenPicker' | 'getOpenDialogAriaText' | 'OpenPickerButtonProps' - | 'parsedDateValue' >; export function useMaskedInput({ diff --git a/lib/src/_shared/hooks/usePickerState.ts b/lib/src/_shared/hooks/usePickerState.ts index 418742abb..162ae627a 100644 --- a/lib/src/_shared/hooks/usePickerState.ts +++ b/lib/src/_shared/hooks/usePickerState.ts @@ -118,10 +118,9 @@ export function usePickerState( inputFormat, open: isOpen, rawValue: value, - parsedDateValue: pickerDate, openPicker: () => !readOnly && !disabled && setIsOpen(true), }), - [onChange, inputFormat, isOpen, value, pickerDate, readOnly, disabled, setIsOpen] + [onChange, inputFormat, isOpen, value, readOnly, disabled, setIsOpen] ); const pickerState = { pickerProps, inputProps, wrapperProps }; diff --git a/lib/src/views/Calendar/Calendar.tsx b/lib/src/views/Calendar/Calendar.tsx index dd78bff74..1c98f0de2 100644 --- a/lib/src/views/Calendar/Calendar.tsx +++ b/lib/src/views/Calendar/Calendar.tsx @@ -8,7 +8,6 @@ import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; import { useGlobalKeyDown, keycode } from '../../_shared/hooks/useKeyDown'; -import { findClosestEnabledDate, DateValidationProps } from '../../_helpers/date-utils'; import { SlideTransition, SlideDirection, SlideTransitionProps } from './SlideTransition'; export interface ExportedCalendarProps @@ -30,10 +29,8 @@ export interface ExportedCalendarProps loadingIndicator?: JSX.Element; } -export interface CalendarProps extends ExportedCalendarProps, DateValidationProps { +export interface CalendarProps extends ExportedCalendarProps { date: MaterialUiPickersDate | MaterialUiPickersDate[]; - minDate: MaterialUiPickersDate; - maxDate: MaterialUiPickersDate; isDateDisabled: (day: MaterialUiPickersDate) => boolean; slideDirection: SlideDirection; currentMonth: MaterialUiPickersDate; @@ -60,6 +57,9 @@ export const useStyles = makeStyles(theme => ({ justifyContent: 'center', alignItems: 'center', }, + weekContainer: { + overflow: 'hidden', + }, week: { margin: `${DAY_MARGIN}px 0`, display: 'flex', @@ -96,11 +96,7 @@ export const Calendar: React.FC = ({ focusedDay, changeFocusedDay, onChange, - minDate, - maxDate, slideDirection, - disableFuture, - disablePast, currentMonth, renderDay, reduceAnimations, @@ -124,21 +120,6 @@ export const Calendar: React.FC = ({ ); const initialDate = Array.isArray(date) ? date[0] : date; - React.useEffect(() => { - if (initialDate && isDateDisabled(initialDate)) { - const closestEnabledDate = findClosestEnabledDate({ - utils, - date: initialDate, - minDate: utils.date(minDate), - maxDate: utils.date(maxDate), - disablePast: Boolean(disablePast), - disableFuture: Boolean(disableFuture), - shouldDisableDate: isDateDisabled, - }); - - handleDaySelect(closestEnabledDate, false); - } - }, []); // eslint-disable-line const nowFocusedDay = focusedDay || initialDate; useGlobalKeyDown(Boolean(allowKeyboardControl), { @@ -181,9 +162,9 @@ export const Calendar: React.FC = ({ className={clsx(classes.transitionContainer, className)} {...TransitionProps} > -

+
{utils.getWeekArray(currentMonth).map(week => ( -
+
{week.map(day => { const disabled = isDateDisabled(day); const isDayInCurrentMonth = utils.getMonth(day) === currentMonthNumber; diff --git a/lib/src/views/Calendar/CalendarView.tsx b/lib/src/views/Calendar/CalendarView.tsx index 13a646c51..9a83a2fbe 100644 --- a/lib/src/views/Calendar/CalendarView.tsx +++ b/lib/src/views/Calendar/CalendarView.tsx @@ -10,12 +10,12 @@ import { VIEW_HEIGHT } from '../../constants/dimensions'; import { MaterialUiPickersDate } from '../../typings/date'; import { FadeTransitionGroup } from './FadeTransitionGroup'; import { Calendar, ExportedCalendarProps } from './Calendar'; -import { DateValidationProps } from '../../_helpers/date-utils'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; import { CalendarHeader, CalendarHeaderProps } from './CalendarHeader'; import { YearSelection, ExportedYearSelectionProps } from './YearSelection'; import { defaultMinDate, defaultMaxDate } from '../../constants/prop-types'; import { IsStaticVariantContext } from '../../wrappers/WrapperVariantContext'; +import { DateValidationProps, findClosestEnabledDate } from '../../_helpers/date-utils'; type PublicCalendarHeaderProps = Pick< CalendarHeaderProps, @@ -110,6 +110,24 @@ export const CalendarView: React.FC = ({ disableFuture, }); + React.useEffect(() => { + if (date && isDateDisabled(date)) { + const closestEnabledDate = findClosestEnabledDate({ + utils, + date, + minDate: utils.date(minDate), + maxDate: utils.date(maxDate), + disablePast: Boolean(disablePast), + disableFuture: Boolean(disableFuture), + shouldDisableDate: isDateDisabled, + }); + + onChange(closestEnabledDate, false); + } + // This call is too expensive to run it on each prop change. + // So just ensure that we are not rendering disabled as selected on mount. + }, []); // eslint-disable-line + React.useEffect(() => { changeMonth(date); }, [date]); // eslint-disable-line @@ -186,8 +204,6 @@ export const CalendarView: React.FC = ({ reduceAnimations={reduceAnimations} date={date} onChange={onChange} - minDate={minDate} - maxDate={maxDate} isDateDisabled={isDateDisabled} allowKeyboardControl={allowKeyboardControl} />