diff --git a/docs/_shared/Code.tsx b/docs/_shared/Code.tsx index 8d898e0b6..fb96cbdf0 100644 --- a/docs/_shared/Code.tsx +++ b/docs/_shared/Code.tsx @@ -42,7 +42,7 @@ interface CodeProps { language?: 'jsx' | 'typescript' | 'markup'; } -const Code: React.SFC = ({ language = 'jsx', inline, children, withMargin }) => { +const Code: React.FC = ({ language = 'typescript', inline, children, withMargin }) => { const classes = useStyles(); const highlightedCode = highlight(children, language); diff --git a/docs/layout/PageWithContext.tsx b/docs/layout/PageWithContext.tsx index 4a1e8d81f..a7b0bc113 100644 --- a/docs/layout/PageWithContext.tsx +++ b/docs/layout/PageWithContext.tsx @@ -9,9 +9,15 @@ import { PageContext } from '../utils/getPageContext'; import { LocalizationProvider } from '@material-ui/pickers'; import { UtilsContext } from '../_shared/UtilsServiceContext'; import { NotificationManager } from 'utils/NotificationManager'; -import { Theme, createMuiTheme, CssBaseline } from '@material-ui/core'; import { createUtilsService, UtilsLib, utilsMap } from '../utils/utilsService'; -import { ThemeProvider, jssPreset, StylesProvider } from '@material-ui/core/styles'; +import { + Theme, + createMuiTheme, + CssBaseline, + ThemeProvider, + jssPreset, + StylesProvider, +} from '@material-ui/core'; export type ThemeType = 'light' | 'dark'; export type Direction = Theme['direction']; diff --git a/docs/next.config.js b/docs/next.config.js index 0fe3f9c53..b9661d522 100644 --- a/docs/next.config.js +++ b/docs/next.config.js @@ -42,7 +42,7 @@ module.exports = withBundleAnalyzer( // Process examples to inject raw code strings config.module.rules.push({ - test: /\.example\.(js|jsx)$/, + test: /\.example\.(js|jsx|tsx|ts)$/, include: [path.resolve(__dirname, 'pages')], use: [ { loader: 'next-babel-loader' }, diff --git a/docs/package.json b/docs/package.json index 722ebeb33..f22b7f43b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -26,6 +26,7 @@ "@material-ui/pickers": "^4.0.0-alpha.1", "@types/fuzzy-search": "^2.1.0", "@types/isomorphic-fetch": "^0.0.35", + "@types/jss": "^10.0.0", "@types/luxon": "^1.11.0", "@types/next": "^8.0.1", "@types/prismjs": "^1.9.1", diff --git a/docs/pages/demo/datepicker/ServerRequest.example.jsx b/docs/pages/demo/datepicker/ServerRequest.example.jsx index f8178ad61..546be5c3b 100644 --- a/docs/pages/demo/datepicker/ServerRequest.example.jsx +++ b/docs/pages/demo/datepicker/ServerRequest.example.jsx @@ -30,7 +30,7 @@ function ServerRequest() { renderDay={(day, selectedDate, DayComponentProps) => { const date = makeJSDateObject(day); // skip this step, it is required to support date libs const isSelected = - DayComponentProps.isInCurrentMonth && selectedDays.includes(date.getDate()); + DayComponentProps.inCurrentMonth && selectedDays.includes(date.getDate()); return ( diff --git a/docs/pages/demo/datepicker/StaticDatePicker.example.jsx b/docs/pages/demo/datepicker/StaticDatePicker.example.jsx index 41d5442e2..e68a8611d 100644 --- a/docs/pages/demo/datepicker/StaticDatePicker.example.jsx +++ b/docs/pages/demo/datepicker/StaticDatePicker.example.jsx @@ -1,5 +1,12 @@ import React, { useState } from 'react'; +import isWeekend from 'date-fns/isWeekend'; import { StaticDatePicker } from '@material-ui/pickers'; +import { makeJSDateObject } from '../../../utils/helpers'; + +function disableWeekends(date) { + // TODO: replace with implementation for your date library + return isWeekend(makeJSDateObject(date)); +} const StaticDatePickerExample = () => { const [date, handleDateChange] = useState(new Date()); @@ -18,6 +25,7 @@ const StaticDatePickerExample = () => { orientation="landscape" openTo="date" value={date} + shouldDisableDate={disableWeekends} onChange={date => handleDateChange(date)} /> diff --git a/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.jsx b/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.jsx deleted file mode 100644 index b6ffc8123..000000000 --- a/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import React, { useState } from 'react'; -import { DatePicker as DateRangePicker } from '@material-ui/pickers'; - -function BasicDateRangePicker() { - const [selectedDate, handleDateChange] = useState([new Date(), null]); - - return handleDateChange(date)} />; -} - -export default BasicDateRangePicker; diff --git a/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.tsx b/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.tsx new file mode 100644 index 000000000..8776a36b8 --- /dev/null +++ b/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { DateRangePicker, DateRange } from '@material-ui/pickers'; + +function BasicDateRangePicker() { + const [selectedDate, handleDateChange] = React.useState([null, null]); + + return ( + handleDateChange(date)} + /> + ); +} + +export default BasicDateRangePicker; diff --git a/docs/pages/demo/daterangepicker/CalendarsDateRangePicker.example.tsx b/docs/pages/demo/daterangepicker/CalendarsDateRangePicker.example.tsx new file mode 100644 index 000000000..eba573ab4 --- /dev/null +++ b/docs/pages/demo/daterangepicker/CalendarsDateRangePicker.example.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import { DateRangePicker, DateRange } from '@material-ui/pickers'; + +function CalendarsDateRangePicker() { + const [selectedDate, handleDateChange] = React.useState([null, null]); + + return ( + + 1 calendar + handleDateChange(date)} + /> + 2 calendars + handleDateChange(date)} + /> + 3 calendars + handleDateChange(date)} + /> + + ); +} + +export default CalendarsDateRangePicker; diff --git a/docs/pages/demo/daterangepicker/MinMaxDateRangePicker.example.tsx b/docs/pages/demo/daterangepicker/MinMaxDateRangePicker.example.tsx new file mode 100644 index 000000000..42402d553 --- /dev/null +++ b/docs/pages/demo/daterangepicker/MinMaxDateRangePicker.example.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import addWeeks from 'date-fns/addWeeks'; +import { Dayjs } from 'dayjs'; +import { Moment } from 'moment'; +import { DateTime } from 'luxon'; +import { makeJSDateObject } from '../../../utils/helpers'; +import { DateRangePicker, DateRange } from '@material-ui/pickers'; + +function getWeeksAfter(date: Moment | DateTime | Dayjs | Date, amount: number) { + // TODO: replace with implementation for your date library + return date ? addWeeks(makeJSDateObject(date), amount) : undefined; +} + +function MinMaxDateRangePicker() { + const [selectedRange, handleDateChange] = React.useState([null, null]); + + return ( + handleDateChange(date)} + /> + ); +} + +export default MinMaxDateRangePicker; diff --git a/docs/pages/demo/daterangepicker/ResponsiveDateRangePicker.example.tsx b/docs/pages/demo/daterangepicker/ResponsiveDateRangePicker.example.tsx new file mode 100644 index 000000000..d0b95f465 --- /dev/null +++ b/docs/pages/demo/daterangepicker/ResponsiveDateRangePicker.example.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { MobileDateRangePicker, DesktopDateRangePicker, DateRange } from '@material-ui/pickers'; + +function ResponsiveDateRangePicker() { + const [selectedDate, handleDateChange] = React.useState([null, null]); + + return ( + <> + handleDateChange(date)} + /> + + handleDateChange(date)} + /> + + ); +} + +export default ResponsiveDateRangePicker; diff --git a/docs/pages/demo/daterangepicker/StaticDateRangePicker.example.tsx b/docs/pages/demo/daterangepicker/StaticDateRangePicker.example.tsx new file mode 100644 index 000000000..1d3ce1e1d --- /dev/null +++ b/docs/pages/demo/daterangepicker/StaticDateRangePicker.example.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { StaticDateRangePicker, DateRange } from '@material-ui/pickers'; + +function StaticDateRangePickerExample() { + const [selectedDate, handleDateChange] = React.useState([null, null]); + + return ( + <> + handleDateChange(date)} + /> + + handleDateChange(date)} + /> + + ); +} + +export default StaticDateRangePickerExample; diff --git a/docs/pages/demo/daterangepicker/index.mdx b/docs/pages/demo/daterangepicker/index.mdx index d1c14f72b..7595c5eb1 100644 --- a/docs/pages/demo/daterangepicker/index.mdx +++ b/docs/pages/demo/daterangepicker/index.mdx @@ -2,8 +2,13 @@ import Ad from '_shared/Ad'; import Example from '_shared/Example'; import PageMeta from '_shared/PageMeta'; import LinkedComponents from '_shared/LinkedComponents'; +import { Hidden } from '@material-ui/core'; import * as BasicDateRangePicker from './BasicDateRangePicker.example'; +import * as ResponsiveDateRangePicker from './ResponsiveDateRangePicker.example'; +import * as MinMaxDateRangePicker from './MinMaxDateRangePicker.example'; +import * as CalendarsDateRangePicker from './CalendarsDateRangePicker.example'; +import * as StaticDateRangePicker from './StaticDateRangePicker.example'; @@ -15,6 +20,33 @@ import * as BasicDateRangePicker from './BasicDateRangePicker.example'; #### Basic usage -Will be rendered to modal dialog on mobile and textfield with popover on desktop. +Basic DateRangePicker example, make sure that you can pass almost any prop of [DatePicker]('/api/DatePicker') - + + +#### Responsiveness + +Date/Time pickers experience is extremely different on mobile and desktop. Here is how components will look on different devices. +The default `DateRangePicker` component is responsive, which means that `Mobile` or `Desktop` mode will be rendered according to device viewport. + + + +#### Different amount of calendars + +Make sure that `calendars` prop is working only for desktop mode. + + + +#### Disabling dates + +Disabling dates performs just like in simple `DatePicker` + + + +#### Static mode + +It is possible to render any picker without modal or popper. For that use `StaticDateRangePicker`. + + + + diff --git a/docs/pages/regression/Regression.tsx b/docs/pages/regression/Regression.tsx index b82351d4f..cb801d8a5 100644 --- a/docs/pages/regression/Regression.tsx +++ b/docs/pages/regression/Regression.tsx @@ -2,9 +2,10 @@ import React, { useState, useContext } from 'react'; import LeftArrowIcon from '@material-ui/icons/KeyboardArrowLeft'; import RightArrowIcon from '@material-ui/icons/KeyboardArrowRight'; import { Grid, Typography } from '@material-ui/core'; -import { MuiPickersContext } from '@material-ui/pickers'; +import { MuiPickersContext, DateRangePicker } from '@material-ui/pickers'; import { createRegressionDay as createRegressionDayRenderer } from './RegressionDay'; import { + DateRange, MobileDatePicker, DesktopDatePicker, MobileTimePicker, @@ -13,6 +14,7 @@ import { function Regression() { const utils = useContext(MuiPickersContext); + const [range, changeRange] = useState([new Date('2019-01-01T00:00:00.000'), null]); const [date, changeDate] = useState(new Date('2019-01-01T00:00:00.000')); const sharedProps = { @@ -64,6 +66,12 @@ function Regression() { + + + DateRangePicker + + + ); } diff --git a/docs/prop-types.json b/docs/prop-types.json index bf58b47f9..f5837f83d 100644 --- a/docs/prop-types.json +++ b/docs/prop-types.json @@ -115,6 +115,21 @@ "type": { "name": "Partial" } + }, + "displayStaticWrapperAs": { + "defaultValue": { + "value": "\"static\"" + }, + "description": "Force static wrapper inner components to be rendered in mobile or desktop mode", + "name": "displayStaticWrapperAs", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/StaticWrapper.tsx", + "name": "StaticWrapperProps" + }, + "required": false, + "type": { + "name": "\"desktop\" | \"mobile\" | \"static\"" + } } }, "DesktopWrapper": { @@ -233,6 +248,47 @@ "type": { "name": "Partial" } + }, + "PopperProps": { + "defaultValue": null, + "description": "Popper props passed to material-ui [Popper](https://material-ui.com/api/popper/#popper-api)", + "name": "PopperProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "TransitionComponent": { + "defaultValue": null, + "description": "Custom component for [transition](https://material-ui.com/components/transitions/#transitioncomponent-prop)", + "name": "TransitionComponent", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "ComponentClass | FunctionComponent" + } + }, + "displayStaticWrapperAs": { + "defaultValue": { + "value": "\"static\"" + }, + "description": "Force static wrapper inner components to be rendered in mobile or desktop mode", + "name": "displayStaticWrapperAs", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/StaticWrapper.tsx", + "name": "StaticWrapperProps" + }, + "required": false, + "type": { + "name": "\"desktop\" | \"mobile\" | \"static\"" + } } }, "DatePicker": { @@ -430,7 +486,7 @@ }, "required": false, "type": { - "name": "ComponentClass, any> | FunctionComponent>" + "name": "ComponentClass, any> | FunctionComponent>" } }, "toolbarTitle": { @@ -668,32 +724,30 @@ "name": "boolean" } }, - "onMonthChange": { + "reduceAnimations": { "defaultValue": null, - "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved", - "name": "onMonthChange", + "description": "Disable heavy animations @default /(android)/i.test(window.navigator.userAgent)", + "name": "reduceAnimations", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", "name": "CalendarViewProps" }, "required": false, "type": { - "name": "(date: DateIOType) => void | Promise" + "name": "boolean" } }, - "reduceAnimations": { - "defaultValue": { - "value": "/(android)/i.test(navigator.userAgent)" - }, - "description": "Do not show heavy animations, significantly improves performance on slow devices", - "name": "reduceAnimations", + "onMonthChange": { + "defaultValue": null, + "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved", + "name": "onMonthChange", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", "name": "CalendarViewProps" }, "required": false, "type": { - "name": "boolean" + "name": "(date: DateIOType) => void | Promise" } }, "shouldDisableDate": { @@ -732,7 +786,7 @@ }, "required": false, "type": { - "name": "(day: DateIOType, selectedDate: DateIOType, DayComponentProps: DayProps) => Element" + "name": "(day: DateIOType, selectedDates: DateIOType[], DayComponentProps: DayProps) => Element" } }, "allowKeyboardControl": { @@ -886,12 +940,12 @@ "name": "(str: string) => string" } }, - "hideOpenPickerButton": { + "disableOpenPicker": { "defaultValue": { "value": "false" }, "description": "Do not render open picker button (renders only text field with validation)", - "name": "hideOpenPickerButton", + "name": "disableOpenPicker", "parent": { "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", "name": "DateInputProps" @@ -1032,6 +1086,47 @@ "name": "Partial" } }, + "PopperProps": { + "defaultValue": null, + "description": "Popper props passed to material-ui [Popper](https://material-ui.com/api/popper/#popper-api)", + "name": "PopperProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "TransitionComponent": { + "defaultValue": null, + "description": "Custom component for [transition](https://material-ui.com/components/transitions/#transitioncomponent-prop)", + "name": "TransitionComponent", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "ComponentClass | FunctionComponent" + } + }, + "displayStaticWrapperAs": { + "defaultValue": { + "value": "\"static\"" + }, + "description": "Force static wrapper inner components to be rendered in mobile or desktop mode", + "name": "displayStaticWrapperAs", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/StaticWrapper.tsx", + "name": "StaticWrapperProps" + }, + "required": false, + "type": { + "name": "\"desktop\" | \"mobile\" | \"static\"" + } + }, "PopoverProps": { "defaultValue": null, "description": "Popover props passed to material-ui Popover", @@ -1062,7 +1157,7 @@ }, "dateAdapter": { "defaultValue": null, - "description": "Allows to pass configured date-io adapter directly. More info [here](/guides/date-adapter-passing)\n```jsx\ndateAdapter={new DateFnsAdapter({ locale: ruLocale })}\n```", + "description": "Allows to pass configured date-io adapter directly. More info [here](https://material-ui-pickers.dev/guides/date-adapter-passing)\n```jsx\ndateAdapter={new DateFnsAdapter({ locale: ruLocale })}\n```", "name": "dateAdapter", "parent": { "fileName": "material-ui-pickers/lib/src/_shared/withDateAdapterProp.tsx", @@ -1329,7 +1424,7 @@ }, "required": false, "type": { - "name": "ComponentClass, any> | FunctionComponent>" + "name": "ComponentClass, any> | FunctionComponent>" } }, "toolbarTitle": { @@ -1511,12 +1606,12 @@ "name": "(str: string) => string" } }, - "hideOpenPickerButton": { + "disableOpenPicker": { "defaultValue": { "value": "false" }, "description": "Do not render open picker button (renders only text field with validation)", - "name": "hideOpenPickerButton", + "name": "disableOpenPicker", "parent": { "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", "name": "DateInputProps" @@ -1767,6 +1862,47 @@ "name": "Partial" } }, + "PopperProps": { + "defaultValue": null, + "description": "Popper props passed to material-ui [Popper](https://material-ui.com/api/popper/#popper-api)", + "name": "PopperProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "TransitionComponent": { + "defaultValue": null, + "description": "Custom component for [transition](https://material-ui.com/components/transitions/#transitioncomponent-prop)", + "name": "TransitionComponent", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "ComponentClass | FunctionComponent" + } + }, + "displayStaticWrapperAs": { + "defaultValue": { + "value": "\"static\"" + }, + "description": "Force static wrapper inner components to be rendered in mobile or desktop mode", + "name": "displayStaticWrapperAs", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/StaticWrapper.tsx", + "name": "StaticWrapperProps" + }, + "required": false, + "type": { + "name": "\"desktop\" | \"mobile\" | \"static\"" + } + }, "PopoverProps": { "defaultValue": null, "description": "Popover props passed to material-ui Popover", @@ -1797,7 +1933,7 @@ }, "dateAdapter": { "defaultValue": null, - "description": "Allows to pass configured date-io adapter directly. More info [here](/guides/date-adapter-passing)\n```jsx\ndateAdapter={new DateFnsAdapter({ locale: ruLocale })}\n```", + "description": "Allows to pass configured date-io adapter directly. More info [here](https://material-ui-pickers.dev/guides/date-adapter-passing)\n```jsx\ndateAdapter={new DateFnsAdapter({ locale: ruLocale })}\n```", "name": "dateAdapter", "parent": { "fileName": "material-ui-pickers/lib/src/_shared/withDateAdapterProp.tsx", @@ -2064,7 +2200,7 @@ }, "required": false, "type": { - "name": "ComponentClass, any> | FunctionComponent>" + "name": "ComponentClass, any> | FunctionComponent>" } }, "toolbarTitle": { @@ -2302,32 +2438,30 @@ "name": "boolean" } }, - "onMonthChange": { + "reduceAnimations": { "defaultValue": null, - "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved", - "name": "onMonthChange", + "description": "Disable heavy animations @default /(android)/i.test(window.navigator.userAgent)", + "name": "reduceAnimations", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", "name": "CalendarViewProps" }, "required": false, "type": { - "name": "(date: DateIOType) => void | Promise" + "name": "boolean" } }, - "reduceAnimations": { - "defaultValue": { - "value": "/(android)/i.test(navigator.userAgent)" - }, - "description": "Do not show heavy animations, significantly improves performance on slow devices", - "name": "reduceAnimations", + "onMonthChange": { + "defaultValue": null, + "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved", + "name": "onMonthChange", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", "name": "CalendarViewProps" }, "required": false, "type": { - "name": "boolean" + "name": "(date: DateIOType) => void | Promise" } }, "shouldDisableDate": { @@ -2366,7 +2500,7 @@ }, "required": false, "type": { - "name": "(day: DateIOType, selectedDate: DateIOType, DayComponentProps: DayProps) => Element" + "name": "(day: DateIOType, selectedDates: DateIOType[], DayComponentProps: DayProps) => Element" } }, "allowKeyboardControl": { @@ -2507,12 +2641,12 @@ "name": "(str: string) => string" } }, - "hideOpenPickerButton": { + "disableOpenPicker": { "defaultValue": { "value": "false" }, "description": "Do not render open picker button (renders only text field with validation)", - "name": "hideOpenPickerButton", + "name": "disableOpenPicker", "parent": { "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", "name": "DateInputProps" @@ -2828,6 +2962,47 @@ "name": "Partial" } }, + "PopperProps": { + "defaultValue": null, + "description": "Popper props passed to material-ui [Popper](https://material-ui.com/api/popper/#popper-api)", + "name": "PopperProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "TransitionComponent": { + "defaultValue": null, + "description": "Custom component for [transition](https://material-ui.com/components/transitions/#transitioncomponent-prop)", + "name": "TransitionComponent", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "ComponentClass | FunctionComponent" + } + }, + "displayStaticWrapperAs": { + "defaultValue": { + "value": "\"static\"" + }, + "description": "Force static wrapper inner components to be rendered in mobile or desktop mode", + "name": "displayStaticWrapperAs", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/StaticWrapper.tsx", + "name": "StaticWrapperProps" + }, + "required": false, + "type": { + "name": "\"desktop\" | \"mobile\" | \"static\"" + } + }, "PopoverProps": { "defaultValue": null, "description": "Popover props passed to material-ui Popover", @@ -2858,7 +3033,7 @@ }, "dateAdapter": { "defaultValue": null, - "description": "Allows to pass configured date-io adapter directly. More info [here](/guides/date-adapter-passing)\n```jsx\ndateAdapter={new DateFnsAdapter({ locale: ruLocale })}\n```", + "description": "Allows to pass configured date-io adapter directly. More info [here](https://material-ui-pickers.dev/guides/date-adapter-passing)\n```jsx\ndateAdapter={new DateFnsAdapter({ locale: ruLocale })}\n```", "name": "dateAdapter", "parent": { "fileName": "material-ui-pickers/lib/src/_shared/withDateAdapterProp.tsx", @@ -2930,20 +3105,966 @@ } } }, - "Calendar": { - "date": { + "DateRangePicker": { + "mask": { "defaultValue": null, - "description": "Calendar Date", - "name": "date", + "description": "Custom mask. Can be used to override generate from format. (e.g. __/__/____ __:__)", + "name": "mask", "parent": { - "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", - "name": "ExportedCalendarProps" + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "string" + } + }, + "onChange": { + "defaultValue": null, + "description": "onChange callback", + "name": "onChange", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" }, "required": true, "type": { - "name": "DateIOType" + "name": "(date: DateRange, keyboardInputValue?: string) => void" + } + }, + "inputFormat": { + "defaultValue": null, + "description": "Format string", + "name": "inputFormat", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "string" + } + }, + "open": { + "defaultValue": null, + "description": "Controlled picker open state", + "name": "open", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "onError": { + "defaultValue": null, + "description": "Callback fired when new error should be displayed\n(!! This is a side effect. Be careful if you want to rerender the component)", + "name": "onError", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "((error: ReactNode, value: DateIOType) => void) & ((error: ReactNode, value: RangeInput | DateRange) => void)" } }, + "value": { + "defaultValue": null, + "description": "Picker value", + "name": "value", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": true, + "type": { + "name": "RangeInput" + } + }, + "disabled": { + "defaultValue": null, + "description": "Disable picker and text field", + "name": "disabled", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "readOnly": { + "defaultValue": null, + "description": "Make picker read only", + "name": "readOnly", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "TextFieldComponent": { + "defaultValue": null, + "description": "Override input component", + "name": "TextFieldComponent", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "ComponentClass | FunctionComponent" + } + }, + "emptyInputText": { + "defaultValue": { + "value": "' '" + }, + "description": "Message displaying in read-only text field when null passed", + "name": "emptyInputText", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "string" + } + }, + "keyboardIcon": { + "defaultValue": null, + "description": "Icon displaying for open picker button", + "name": "keyboardIcon", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "maskChar": { + "defaultValue": { + "value": "'_'" + }, + "description": "Char string that will be replaced with number (for \"_\" mask will be \"__/__/____\")", + "name": "maskChar", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "string" + } + }, + "acceptRegex": { + "defaultValue": { + "value": "/\\dap/gi" + }, + "description": "Regular expression to detect \"accepted\" symbols", + "name": "acceptRegex", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "RegExp" + } + }, + "InputAdornmentProps": { + "defaultValue": null, + "description": "Props to pass to keyboard input adornment", + "name": "InputAdornmentProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "KeyboardButtonProps": { + "defaultValue": null, + "description": "Props to pass to keyboard adornment button", + "name": "KeyboardButtonProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "rifmFormatter": { + "defaultValue": null, + "description": "Custom formatter to be passed into Rifm component", + "name": "rifmFormatter", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "(str: string) => string" + } + }, + "disableOpenPicker": { + "defaultValue": { + "value": "false" + }, + "description": "Do not render open picker button (renders only text field with validation)", + "name": "disableOpenPicker", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "disableMaskedInput": { + "defaultValue": { + "value": "false" + }, + "description": "Disable mask on the keyboard, this should be used rarely. Consider passing proper mask for your format", + "name": "disableMaskedInput", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "getOpenDialogAriaText": { + "defaultValue": null, + "description": "Get aria-label text for control that opens datepicker dialog. Aria-label have to include selected date.", + "name": "getOpenDialogAriaText", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/PureDateInput.tsx", + "name": "DateInputProps" + }, + "required": false, + "type": { + "name": "(value: any, utils: MuiPickersAdapter) => string" + } + }, + "onAccept": { + "defaultValue": null, + "description": "Callback fired when date is accepted", + "name": "onAccept", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "((date: DateIOType) => void) & ((date: DateRange) => void)" + } + }, + "okLabel": { + "defaultValue": { + "value": "\"OK\"" + }, + "description": "\"OK\" label message", + "name": "okLabel", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/MobileWrapper.tsx", + "name": "InnerMobileWrapperProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "cancelLabel": { + "defaultValue": { + "value": "\"CANCEL\"" + }, + "description": "\"CANCEL\" label message", + "name": "cancelLabel", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/MobileWrapper.tsx", + "name": "InnerMobileWrapperProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "clearLabel": { + "defaultValue": { + "value": "\"CLEAR\"" + }, + "description": "\"CLEAR\" label message", + "name": "clearLabel", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/MobileWrapper.tsx", + "name": "InnerMobileWrapperProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "todayLabel": { + "defaultValue": { + "value": "\"TODAY\"" + }, + "description": "\"TODAY\" label message", + "name": "todayLabel", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/MobileWrapper.tsx", + "name": "InnerMobileWrapperProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "showTodayButton": { + "defaultValue": { + "value": "false" + }, + "description": "If true today button will be displayed. **Note** that clear button has higher priority", + "name": "showTodayButton", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/MobileWrapper.tsx", + "name": "InnerMobileWrapperProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "clearable": { + "defaultValue": { + "value": "false" + }, + "description": "Show clear action in picker dialog", + "name": "clearable", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/MobileWrapper.tsx", + "name": "InnerMobileWrapperProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "DialogProps": { + "defaultValue": null, + "description": "Props to be passed directly to material-ui Dialog", + "name": "DialogProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/MobileWrapper.tsx", + "name": "InnerMobileWrapperProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "PopperProps": { + "defaultValue": null, + "description": "Popper props passed to material-ui [Popper](https://material-ui.com/api/popper/#popper-api)", + "name": "PopperProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "TransitionComponent": { + "defaultValue": null, + "description": "Custom component for [transition](https://material-ui.com/components/transitions/#transitioncomponent-prop)", + "name": "TransitionComponent", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopPopperWrapper.tsx", + "name": "InnerDesktopPopperWrapperProps" + }, + "required": false, + "type": { + "name": "ComponentClass | FunctionComponent" + } + }, + "displayStaticWrapperAs": { + "defaultValue": { + "value": "\"static\"" + }, + "description": "Force static wrapper inner components to be rendered in mobile or desktop mode", + "name": "displayStaticWrapperAs", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/StaticWrapper.tsx", + "name": "StaticWrapperProps" + }, + "required": false, + "type": { + "name": "\"desktop\" | \"mobile\" | \"static\"" + } + }, + "PopoverProps": { + "defaultValue": null, + "description": "Popover props passed to material-ui Popover", + "name": "PopoverProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/DesktopWrapper.tsx", + "name": "InnerDesktopWrapperProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "onClose": { + "defaultValue": null, + "description": "On close callback", + "name": "onClose", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "() => void" + } + }, + "desktopModeBreakpoint": { + "defaultValue": { + "value": "'md'" + }, + "description": "Breakpoint when `Desktop` mode will be changed to `Mobile`", + "name": "desktopModeBreakpoint", + "parent": { + "fileName": "material-ui-pickers/lib/src/wrappers/ResponsiveWrapper.tsx", + "name": "ResponsiveWrapperProps" + }, + "required": false, + "type": { + "name": "\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"" + } + }, + "disableHighlightToday": { + "defaultValue": { + "value": "false" + }, + "description": "Disable highlighting today date with a circle", + "name": "disableHighlightToday", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/Day.tsx", + "name": "DayProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "showDaysOutsideCurrentMonth": { + "defaultValue": { + "value": "false" + }, + "description": "Display disabled dates outside the current month", + "name": "showDaysOutsideCurrentMonth", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/Day.tsx", + "name": "DayProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "leftArrowIcon": { + "defaultValue": null, + "description": "Left arrow icon", + "name": "leftArrowIcon", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/ArrowSwitcher.tsx", + "name": "ExportedArrowSwitcherProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "rightArrowIcon": { + "defaultValue": null, + "description": "Right arrow icon", + "name": "rightArrowIcon", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/ArrowSwitcher.tsx", + "name": "ExportedArrowSwitcherProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "leftArrowButtonProps": { + "defaultValue": null, + "description": "Props to pass to left arrow button", + "name": "leftArrowButtonProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/ArrowSwitcher.tsx", + "name": "ExportedArrowSwitcherProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "rightArrowButtonProps": { + "defaultValue": null, + "description": "Props to pass to right arrow button", + "name": "rightArrowButtonProps", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/ArrowSwitcher.tsx", + "name": "ExportedArrowSwitcherProps" + }, + "required": false, + "type": { + "name": "Partial" + } + }, + "leftArrowButtonText": { + "defaultValue": null, + "description": "Left arrow icon aria-label text", + "name": "leftArrowButtonText", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/ArrowSwitcher.tsx", + "name": "ExportedArrowSwitcherProps" + }, + "required": false, + "type": { + "name": "string" + } + }, + "rightArrowButtonText": { + "defaultValue": null, + "description": "Right arrow icon aria-label text", + "name": "rightArrowButtonText", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/ArrowSwitcher.tsx", + "name": "ExportedArrowSwitcherProps" + }, + "required": false, + "type": { + "name": "string" + } + }, + "getViewSwitchingButtonText": { + "defaultValue": null, + "description": "Get aria-label text for switching between views button", + "name": "getViewSwitchingButtonText", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarHeader.tsx", + "name": "CalendarHeaderProps" + }, + "required": false, + "type": { + "name": "(currentView: DatePickerView) => string" + } + }, + "minDate": { + "defaultValue": { + "value": "Date(1900-01-01)" + }, + "description": "Min selectable date", + "name": "minDate", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "any" + } + }, + "maxDate": { + "defaultValue": { + "value": "Date(2100-01-01)" + }, + "description": "Max selectable date", + "name": "maxDate", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "any" + } + }, + "disablePast": { + "defaultValue": { + "value": "false" + }, + "description": "Disable past dates", + "name": "disablePast", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", + "name": "ExportedCalendarProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "disableFuture": { + "defaultValue": { + "value": "false" + }, + "description": "Disable future dates", + "name": "disableFuture", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", + "name": "ExportedCalendarProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "reduceAnimations": { + "defaultValue": null, + "description": "Disable heavy animations @default /(android)/i.test(window.navigator.userAgent)", + "name": "reduceAnimations", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "onMonthChange": { + "defaultValue": null, + "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved", + "name": "onMonthChange", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "(date: DateIOType) => void | Promise" + } + }, + "shouldDisableDate": { + "defaultValue": null, + "description": "Disable specific date", + "name": "shouldDisableDate", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "(day: DateIOType) => boolean" + } + }, + "renderDay": { + "defaultValue": null, + "description": "Custom renderer for day. Check [DayComponentProps api](https://material-ui-pickers.dev/api/Day)", + "name": "renderDay", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", + "name": "ExportedCalendarProps" + }, + "required": false, + "type": { + "name": "(day: DateIOType, selectedDates: DateIOType[], DayComponentProps: DayProps) => Element" + } + }, + "allowKeyboardControl": { + "defaultValue": { + "value": "currentWrapper !== 'static'" + }, + "description": "Enables keyboard listener for moving between days in calendar", + "name": "allowKeyboardControl", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", + "name": "ExportedCalendarProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "loadingIndicator": { + "defaultValue": null, + "description": "Custom loading indicator", + "name": "loadingIndicator", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/Calendar.tsx", + "name": "ExportedCalendarProps" + }, + "required": false, + "type": { + "name": "Element" + } + }, + "autoOk": { + "defaultValue": { + "value": "false" + }, + "description": "Auto accept date on selection", + "name": "autoOk", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "defaultHighlight": { + "defaultValue": null, + "description": "Date that will be initially highlighted if null was passed", + "name": "defaultHighlight", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "any" + } + }, + "onOpen": { + "defaultValue": null, + "description": "On open callback", + "name": "onOpen", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "() => void" + } + }, + "showToolbar": { + "defaultValue": null, + "description": "Show toolbar even in desktop mode", + "name": "showToolbar", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "orientation": { + "defaultValue": null, + "description": "Force rendering in particular orientation", + "name": "orientation", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "\"portrait\" | \"landscape\"" + } + }, + "ToolbarComponent": { + "defaultValue": null, + "description": "Component that will replace default toolbar renderer", + "name": "ToolbarComponent", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "ComponentClass, any> | FunctionComponent>" + } + }, + "toolbarTitle": { + "defaultValue": { + "value": "\"SELECT DATE\"" + }, + "description": "Mobile picker title, displaying in the toolbar", + "name": "toolbarTitle", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "toolbarFormat": { + "defaultValue": null, + "description": "Date format, that is displaying in toolbar", + "name": "toolbarFormat", + "parent": { + "fileName": "material-ui-pickers/lib/src/typings/BasePicker.tsx", + "name": "BasePickerProps" + }, + "required": false, + "type": { + "name": "string" + } + }, + "startText": { + "defaultValue": { + "value": "\"Start\"" + }, + "description": "Text for start input label and toolbar placeholder", + "name": "startText", + "parent": { + "fileName": "material-ui-pickers/lib/src/DateRangePicker/DateRangePicker.tsx", + "name": "DateRangePickerProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "endText": { + "defaultValue": { + "value": "\"end\"" + }, + "description": "Text for end input label and toolbar placeholder", + "name": "endText", + "parent": { + "fileName": "material-ui-pickers/lib/src/DateRangePicker/DateRangePicker.tsx", + "name": "DateRangePickerProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "disableAutoMonthSwitching": { + "defaultValue": { + "value": "false" + }, + "description": "if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date", + "name": "disableAutoMonthSwitching", + "parent": { + "fileName": "material-ui-pickers/lib/src/DateRangePicker/DateRangePickerView.tsx", + "name": "ExportedDateRangePickerViewProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "calendars": { + "defaultValue": { + "value": "2" + }, + "description": "How many calendars render on **desktop** DateRangePicker", + "name": "calendars", + "parent": { + "fileName": "material-ui-pickers/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx", + "name": "ExportedDesktopDateRangeCalendarProps" + }, + "required": false, + "type": { + "name": "2 | 1 | 3" + } + }, + "dateAdapter": { + "defaultValue": null, + "description": "Allows to pass configured date-io adapter directly. More info [here](https://material-ui-pickers.dev/guides/date-adapter-passing)\n```jsx\ndateAdapter={new DateFnsAdapter({ locale: ruLocale })}\n```", + "name": "dateAdapter", + "parent": { + "fileName": "material-ui-pickers/lib/src/_shared/withDateAdapterProp.tsx", + "name": "WithDateAdapterProps" + }, + "required": false, + "type": { + "name": "MuiPickersAdapter" + } + }, + "minDateMessage": { + "defaultValue": { + "value": "'Date should not be before minimal date'" + }, + "description": "Error message, shown if date is less then minimal date", + "name": "minDateMessage", + "parent": { + "fileName": "material-ui-pickers/lib/src/_helpers/text-field-helper.ts", + "name": "DateValidationProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "maxDateMessage": { + "defaultValue": { + "value": "'Date should not be after maximal date'" + }, + "description": "Error message, shown if date is more then maximal date", + "name": "maxDateMessage", + "parent": { + "fileName": "material-ui-pickers/lib/src/_helpers/text-field-helper.ts", + "name": "DateValidationProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + }, + "strictCompareDates": { + "defaultValue": { + "value": "false" + }, + "description": "Compare dates by the exact timestamp, instead of start/end of date", + "name": "strictCompareDates", + "parent": { + "fileName": "material-ui-pickers/lib/src/_helpers/text-field-helper.ts", + "name": "DateValidationProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "invalidDateMessage": { + "defaultValue": { + "value": "'Invalid Date Format'" + }, + "description": "Message, appearing when date cannot be parsed", + "name": "invalidDateMessage", + "parent": { + "fileName": "material-ui-pickers/lib/src/_helpers/text-field-helper.ts", + "name": "BaseValidationProps" + }, + "required": false, + "type": { + "name": "ReactNode" + } + } + }, + "Calendar": { "onChange": { "defaultValue": null, "description": "Calendar onChange", @@ -2997,7 +4118,7 @@ }, "required": false, "type": { - "name": "(day: DateIOType, selectedDate: DateIOType, DayComponentProps: DayProps) => Element" + "name": "(day: DateIOType, selectedDates: DateIOType[], DayComponentProps: DayProps) => Element" } }, "allowKeyboardControl": { @@ -3103,10 +4224,10 @@ "name": "boolean" } }, - "isInCurrentMonth": { + "inCurrentMonth": { "defaultValue": null, "description": "Is day in current month", - "name": "isInCurrentMonth", + "name": "inCurrentMonth", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/Day.tsx", "name": "DayProps" @@ -3129,12 +4250,12 @@ "name": "boolean" } }, - "isToday": { + "today": { "defaultValue": { "value": false }, "description": "Is today?", - "name": "isToday", + "name": "today", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/Day.tsx", "name": "DayProps" diff --git a/docs/scripts/docgen.js b/docs/scripts/docgen.js index a2de7c0e2..0acd01e1f 100644 --- a/docs/scripts/docgen.js +++ b/docs/scripts/docgen.js @@ -26,10 +26,9 @@ const components = [ 'wrappers/MobileWrapper.tsx', 'wrappers/DesktopWrapper.tsx', 'DatePicker/DatePicker.tsx', - 'DatePicker/KeyboardDatePicker.tsx', 'TimePicker/TimePicker.tsx', 'DateTimePicker/DateTimePicker.tsx', - 'DateTimePicker/KeyboardDateTimePicker.tsx', + 'DateRangePicker/DateRangePicker.tsx', // internal components 'views/Calendar/Calendar.tsx', 'views/Calendar/Day.tsx', diff --git a/e2e/integration/DateRange.spec.ts b/e2e/integration/DateRange.spec.ts new file mode 100644 index 000000000..f1eba60fc --- /dev/null +++ b/e2e/integration/DateRange.spec.ts @@ -0,0 +1,122 @@ +describe('DateRangePicker', () => { + beforeEach(() => { + cy.visit('/regression'); + cy.viewport('macbook-13'); + }); + + it('Opens and selecting a range in DateRangePicker', () => { + cy.get('#desktop-range-picker input') + .first() + .focus(); + cy.get('[aria-label="Jan 1, 2019"]').click(); + cy.get('[aria-label="Jan 24, 2019"]').click(); + + cy.get('[data-mui-test="DateRangeHighlight"]').should('have.length', 24); + }); + + it('Opens and selecting a range on the next month', () => { + cy.get('#desktop-range-picker input') + .first() + .focus(); + + cy.get('[aria-label="Jan 1, 2019"]').click(); + cy.get('[data-mui-test="next-arrow-button"]') + .eq(1) + .click(); + + cy.get('[aria-label="Mar 19, 2019"]').click(); + + cy.get('[data-mui-test="DateRangeHighlight"]').should('have.length', 47); + }); + + it.skip('Shows range preview on hover', () => { + cy.get('#desktop-range-picker input') + .first() + .focus(); + + cy.get('[aria-label="Jan 24, 2019"').trigger('mouseover'); + }); + + it('Properly handles selection when starting from end', () => { + cy.get('#desktop-range-picker input') + .first() + .clear(); + + cy.get('#desktop-range-picker input') + .eq(1) + .focus(); + + cy.get('[aria-label="Jan 30, 2019"]') + .first() + .click(); + cy.get('[aria-label="Jan 19, 2019"]').click(); + + cy.get('[data-mui-test="DateRangeHighlight"]').should('have.length', 12); + + cy.get('[aria-label="Jan 24, 2019"]') + .first() + .click(); + cy.get('div[role="tooltip"]').should('not.be.visible'); + }); + + it('Allows pure keyboard input control', () => { + cy.get('#desktop-range-picker input') + .eq(0) + .clear() + .type('06/06/2019'); + + cy.contains('June 2019'); + cy.contains('July 2019'); + + cy.get('#desktop-range-picker input') + .eq(1) + .focus() + .clear() + .type('08/08/2019'); + + cy.contains('July 2019'); + cy.contains('August 2019'); + + cy.get('[data-mui-test="DateRangeHighlight"]').should('have.length', 39); + }); + + it('Scrolls current month to the active selection on focusing appropriate field', () => { + cy.get('#desktop-range-picker input') + .first() + .click(); + + cy.get('[aria-label="Jan 19, 2019"]').click(); + + cy.get('[data-mui-test="next-arrow-button"]') + .eq(1) + .click() + .click(); + + cy.get('#desktop-range-picker input') + .first() + .click(); + + cy.contains('January 2019'); + }); + + it('Opens on the current selecting range end', () => { + cy.get('#desktop-range-picker input') + .first() + .click(); + + cy.get('[aria-label="Jan 19, 2019"]').click(); + cy.get('[data-mui-test="next-arrow-button"]') + .eq(1) + .click(); + + cy.get('[aria-label="Mar 19, 2019"]').click(); + + // reopen picker + cy.get('#desktop-range-picker input') + .eq(1) + .click(); + + cy.contains('February 2019'); + cy.contains('March 2019'); + }); +}); diff --git a/lib/.size-snapshot.json b/lib/.size-snapshot.json index aeefd56ac..b9c9ca289 100644 --- a/lib/.size-snapshot.json +++ b/lib/.size-snapshot.json @@ -1,26 +1,26 @@ { "build/dist/material-ui-pickers.esm.js": { - "bundled": 145384, - "minified": 79576, - "gzipped": 21474, + "bundled": 190777, + "minified": 103329, + "gzipped": 26701, "treeshaked": { "rollup": { - "code": 65692, - "import_statements": 2099 + "code": 85805, + "import_statements": 2112 }, "webpack": { - "code": 73171 + "code": 95022 } } }, "build/dist/material-ui-pickers.umd.js": { - "bundled": 599635, - "minified": 223221, - "gzipped": 45558 + "bundled": 300842, + "minified": 118907, + "gzipped": 33715 }, "build/dist/material-ui-pickers.umd.min.js": { - "bundled": 539383, - "minified": 204705, - "gzipped": 40705 + "bundled": 259632, + "minified": 109852, + "gzipped": 30972 } } diff --git a/lib/package.json b/lib/package.json index 6b8815b2c..49ae7c93a 100644 --- a/lib/package.json +++ b/lib/package.json @@ -122,11 +122,11 @@ "setupFilesAfterEnv": [ "/src/__tests__/setup.js" ], - "testRegex": "./src/__tests__/.*\\.test\\.(js|tsx)$", + "testRegex": "./src/__tests__/.*\\.test\\.(js|tsx|ts)$", "testURL": "http://localhost/", "collectCoverage": true, "transform": { - "^.+\\.tsx?$": "ts-jest" + "^.+\\.(ts|tsx)?$": "ts-jest" }, "moduleFileExtensions": [ "ts", diff --git a/lib/rollup.config.js b/lib/rollup.config.js index 6523fbf56..dbddf7262 100644 --- a/lib/rollup.config.js +++ b/lib/rollup.config.js @@ -27,7 +27,7 @@ const globals = { '@material-ui/core/Grid': 'material-ui.Grid', '@material-ui/core/IconButton': 'material-ui.IconButton', '@material-ui/core/InputAdornment': 'material-ui.InputAdornment', - '@material-ui/core/internal/svg-icons/createSvgIcon': 'material-ui.createSvgIcon', + '@material-ui/core/utils': 'material-ui.utils', '@material-ui/core/Paper': 'material-ui.Paper', '@material-ui/core/Popover': 'material-ui.Popover', '@material-ui/core/styles': 'material-ui', @@ -39,6 +39,9 @@ const globals = { '@material-ui/core/Toolbar': 'material-ui.Toolbar', '@material-ui/core/Typography': 'material-ui.Typography', '@material-ui/core/useMediaQuery': 'material-ui.useMediaQuery', + '@material-ui/core/Modal/TrapFocus': 'material-ui.TrapFocus', + '@material-ui/core/Grow': 'material-ui.Grow', + '@material-ui/core/Popper': 'material-ui.Popper', }; const extensions = ['.ts', '.tsx', '.js']; diff --git a/lib/src/DatePicker/DatePickerToolbar.tsx b/lib/src/DatePicker/DatePickerToolbar.tsx index 0b21afdbc..b442f6bde 100644 --- a/lib/src/DatePicker/DatePickerToolbar.tsx +++ b/lib/src/DatePicker/DatePickerToolbar.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import clsx from 'clsx'; -import { makeStyles } from '@material-ui/core/styles'; -import Typography from '@material-ui/core/Typography'; import PickerToolbar from '../_shared/PickerToolbar'; +import Typography from '@material-ui/core/Typography'; import { DatePickerView } from './DatePicker'; import { useUtils } from '../_shared/hooks/useUtils'; +import { makeStyles } from '@material-ui/core/styles'; import { ToolbarComponentProps } from '../Picker/Picker'; import { isYearAndMonthViews, isYearOnlyView } from '../_helpers/date-utils'; diff --git a/lib/src/DateRangePicker/DateRangePicker.tsx b/lib/src/DateRangePicker/DateRangePicker.tsx new file mode 100644 index 000000000..a9f7232e7 --- /dev/null +++ b/lib/src/DateRangePicker/DateRangePicker.tsx @@ -0,0 +1,151 @@ +import * as React from 'react'; +import { MaterialUiPickersDate } from '../typings/date'; +import { BasePickerProps } from '../typings/BasePicker'; +import { MobileWrapper } from '../wrappers/MobileWrapper'; +import { DateRangeInputProps } from './DateRangePickerInput'; +import { parsePickerInputValue } from '../_helpers/date-utils'; +import { usePickerState } from '../_shared/hooks/usePickerState'; +import { AllSharedPickerProps } from '../Picker/SharedPickerProps'; +import { DateRange as DateRangeType, RangeInput } from './RangeTypes'; +import { ResponsivePopperWrapper } from '../wrappers/ResponsiveWrapper'; +import { DesktopPopperWrapper } from '../wrappers/DesktopPopperWrapper'; +import { MuiPickersAdapter, useUtils } from '../_shared/hooks/useUtils'; +import { makeWrapperComponent } from '../wrappers/makeWrapperComponent'; +import { SomeWrapper, ExtendWrapper, StaticWrapper } from '../wrappers/Wrapper'; +import { DateRangePickerView, ExportedDateRangePickerViewProps } from './DateRangePickerView'; +import { DateRangePickerInput, ExportedDateRangePickerInputProps } from './DateRangePickerInput'; + +export function parseRangeInputValue( + now: MaterialUiPickersDate, + utils: MuiPickersAdapter, + { value = [null, null], defaultHighlight }: BasePickerProps +) { + return value.map(date => + date === null + ? null + : utils.startOfDay(parsePickerInputValue(now, utils, { value: date, defaultHighlight })) + ) as DateRangeType; +} + +interface DateRangePickerProps + extends ExportedDateRangePickerViewProps, + ExportedDateRangePickerInputProps { + /** + * Text for start input label and toolbar placeholder + * @default "Start" + */ + startText?: React.ReactNode; + /** + * Text for end input label and toolbar placeholder + * @default "end" + */ + endText?: React.ReactNode; +} + +export function makeRangePicker(Wrapper: TWrapper) { + const WrapperComponent = makeWrapperComponent( + Wrapper, + { + KeyboardDateInputComponent: DateRangePickerInput, + PureDateInputComponent: DateRangePickerInput, + } + ); + + function RangePickerWithStateAndWrapper({ + calendars, + minDate, + maxDate, + disablePast, + disableFuture, + shouldDisableDate, + showDaysOutsideCurrentMonth, + onMonthChange, + disableHighlightToday, + reduceAnimations, + value, + onChange, + mask = '__/__/____', + variant = 'outlined', + startText = 'Start', + endText = 'End', + inputFormat: passedInputFormat, + ...restPropsForTextField + }: DateRangePickerProps & AllSharedPickerProps & ExtendWrapper) { + const utils = useUtils(); + const [currentlySelectingRangeEnd, setCurrentlySelectingRangeEnd] = React.useState< + 'start' | 'end' + >('start'); + + const pickerStateProps = { + ...restPropsForTextField, + value, + onChange, + inputFormat: passedInputFormat || utils.formats.keyboardDate, + }; + + const { pickerProps, inputProps, wrapperProps } = usePickerState( + pickerStateProps, + { + parseInput: parseRangeInputValue, + areValuesEqual: (a, b) => utils.isEqual(a[0], b[0]) && utils.isEqual(a[1], b[1]), + validateInput: () => undefined, + emptyValue: [null, null], + } + ); + + const DateInputProps = { + ...inputProps, + ...restPropsForTextField, + currentlySelectingRangeEnd, + setCurrentlySelectingRangeEnd, + startText, + endText, + mask, + variant, + }; + + return ( + + + + ); + } + + return React.forwardRef< + HTMLDivElement, + React.ComponentProps + >((props, ref) => ); +} + +// TODO replace with new export type syntax +export type DateRange = DateRangeType; + +export const DateRangePicker = makeRangePicker(ResponsivePopperWrapper); + +export const DesktopDateRangePicker = makeRangePicker(DesktopPopperWrapper); + +export const MobileDateRangePicker = makeRangePicker(MobileWrapper); + +export const StaticDateRangePicker = makeRangePicker(StaticWrapper); diff --git a/lib/src/DateRangePicker/DateRangePickerDay.tsx b/lib/src/DateRangePicker/DateRangePickerDay.tsx new file mode 100644 index 000000000..9f54cb312 --- /dev/null +++ b/lib/src/DateRangePicker/DateRangePickerDay.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { DAY_MARGIN } from '../constants/dimensions'; +import { useUtils } from '../_shared/hooks/useUtils'; +import { makeStyles, fade } from '@material-ui/core/styles'; +import { Day, DayProps, areDayPropsEqual } from '../views/Calendar/Day'; + +interface DateRangeDayProps extends DayProps { + isHighlighting: boolean; + isEndOfHighlighting: boolean; + isStartOfHighlighting: boolean; + isPreviewing: boolean; + isEndOfPreviewing: boolean; + isStartOfPreviewing: boolean; +} + +const endBorderStyle = { + borderTopRightRadius: '50%', + borderBottomRightRadius: '50%', +}; + +const startBorderStyle = { + borderTopLeftRadius: '50%', + borderBottomLeftRadius: '50%', +}; + +const useStyles = makeStyles( + theme => ({ + rangeIntervalDay: { + '&:first-child $rangeIntervalDayPreview': { + ...startBorderStyle, + borderLeftColor: theme.palette.divider, + }, + '&:last-child $rangeIntervalDayPreview': { + ...endBorderStyle, + borderRightColor: theme.palette.divider, + }, + }, + rangeIntervalDayHighlight: { + borderRadius: 0, + color: theme.palette.primary.contrastText, + backgroundColor: fade(theme.palette.primary.light, 0.6), + '&:first-child': startBorderStyle, + '&:last-child': endBorderStyle, + }, + rangeIntervalDayHighlightStart: { + ...startBorderStyle, + paddingLeft: 0, + marginLeft: DAY_MARGIN / 2, + }, + rangeIntervalDayHighlightEnd: { + ...endBorderStyle, + paddingRight: 0, + marginRight: DAY_MARGIN / 2, + }, + day: { + // Required to overlap preview border + transform: 'scale(1.1)', + '& > *': { + transform: 'scale(0.9)', + }, + }, + dayOutsideRangeInterval: { + '&:hover': { + border: `1px solid ${theme.palette.grey[500]}`, + }, + }, + dayInsideRangeInterval: { + color: theme.palette.getContrastText(fade(theme.palette.primary.light, 0.6)), + }, + notSelectedDate: { + backgroundColor: 'transparent', + }, + rangeIntervalPreview: { + // replace default day component margin with transparent border to avoid jumping on preview + border: '2px solid transparent', + }, + rangeIntervalDayPreview: { + borderRadius: 0, + border: `2px dashed ${theme.palette.divider}`, + borderLeftColor: 'transparent', + borderRightColor: 'transparent', + + '&$rangeIntervalDayPreviewStart': { + borderLeftColor: theme.palette.divider, + ...startBorderStyle, + }, + '&$rangeIntervalDayPreviewEnd': { + borderRightColor: theme.palette.divider, + ...endBorderStyle, + }, + }, + rangeIntervalDayPreviewStart: {}, + rangeIntervalDayPreviewEnd: {}, + }), + { name: 'MuiPickersDateRangeDay' } +); + +export const PureDateRangeDay = ({ + day, + className, + selected, + isPreviewing, + isStartOfPreviewing, + isEndOfPreviewing, + isHighlighting, + isEndOfHighlighting, + isStartOfHighlighting, + inCurrentMonth, + ...other +}: DateRangeDayProps) => { + const utils = useUtils(); + const classes = useStyles(); + + const isEndOfMonth = utils.isSameDay(day, utils.endOfMonth(day)); + const isStartOfMonth = utils.isSameDay(day, utils.startOfMonth(day)); + + const shouldRenderHighlight = isHighlighting && inCurrentMonth; + const shouldRenderPreview = isPreviewing && inCurrentMonth; + + return ( +
+
+ +
+
+ ); +}; + +PureDateRangeDay.displayName = 'DateRangeDay'; + +export const DateRangeDay = React.memo(PureDateRangeDay, (prevProps, nextProps) => { + return ( + prevProps.isHighlighting === nextProps.isHighlighting && + prevProps.isEndOfHighlighting === nextProps.isEndOfHighlighting && + prevProps.isStartOfHighlighting === nextProps.isStartOfHighlighting && + prevProps.isPreviewing === nextProps.isPreviewing && + prevProps.isEndOfPreviewing === nextProps.isEndOfPreviewing && + prevProps.isStartOfPreviewing === nextProps.isStartOfPreviewing && + areDayPropsEqual(prevProps, nextProps) + ); +}); diff --git a/lib/src/DateRangePicker/DateRangePickerInput.tsx b/lib/src/DateRangePicker/DateRangePickerInput.tsx new file mode 100644 index 000000000..fa8fefc80 --- /dev/null +++ b/lib/src/DateRangePicker/DateRangePickerInput.tsx @@ -0,0 +1,161 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import Typography from '@material-ui/core/Typography'; +import KeyboardDateInput from '../_shared/KeyboardDateInput'; +import { RangeInput, DateRange } from './RangeTypes'; +import { useUtils } from '../_shared/hooks/useUtils'; +import { makeStyles } from '@material-ui/core/styles'; +import { MaterialUiPickersDate } from '../typings/date'; +import { DateInputProps } from '../_shared/PureDateInput'; +import { CurrentlySelectingRangeEndProps } from './RangeTypes'; +import { mergeRefs, createDelegatedEventHandler, doNothing } from '../_helpers/utils'; + +export const useStyles = makeStyles( + theme => ({ + rangeInputsContainer: { + display: 'flex', + alignItems: 'center', + [theme.breakpoints.down('xs')]: { + flexDirection: 'column', + }, + }, + toLabelDelimiter: { + margin: '8px 0', + [theme.breakpoints.up('sm')]: { + margin: '0 16px', + }, + }, + }), + { name: 'MuiPickersDateRangePickerInput' } +); + +export interface ExportedDateRangePickerInputProps { + toText?: React.ReactNode; +} + +export interface DateRangeInputProps + extends ExportedDateRangePickerInputProps, + CurrentlySelectingRangeEndProps, + Omit, 'forwardedRef'> { + startText: React.ReactNode; + endText: React.ReactNode; + forwardedRef?: React.Ref; + containerRef?: React.Ref; +} + +export const DateRangePickerInput: React.FC = ({ + toText = 'to', + rawValue, + onChange, + onClick, + parsedDateValue: [start, end], + id, + open, + className, + containerRef, + forwardedRef, + currentlySelectingRangeEnd, + setCurrentlySelectingRangeEnd, + openPicker, + onFocus, + readOnly, + disableOpenPicker, + startText, + endText, + ...other +}) => { + const utils = useUtils(); + const classes = useStyles(); + const startRef = React.useRef(null); + const endRef = React.useRef(null); + + React.useEffect(() => { + if (!open) { + return; + } + + if (currentlySelectingRangeEnd === 'start') { + startRef.current?.focus(); + } else if (currentlySelectingRangeEnd === 'end') { + endRef.current?.focus(); + } + }, [currentlySelectingRangeEnd, open]); + + const handleStartChange = (date: MaterialUiPickersDate, inputString?: string) => { + if (date === null || utils.isValid(date)) { + onChange([date, end], inputString); + } + }; + + const handleEndChange = (date: MaterialUiPickersDate, inputString?: string) => { + if (date === null || utils.isValid(date)) { + onChange([start, date], inputString); + } + }; + + const openRangeStartSelection = () => { + if (setCurrentlySelectingRangeEnd) { + setCurrentlySelectingRangeEnd('start'); + } + if (!disableOpenPicker) { + openPicker(); + } + }; + + const openRangeEndSelection = () => { + if (setCurrentlySelectingRangeEnd) { + setCurrentlySelectingRangeEnd('end'); + } + if (!disableOpenPicker) { + openPicker(); + } + }; + + return ( +
+ + + {toText} + + +
+ ); +}; diff --git a/lib/src/DateRangePicker/DateRangePickerToolbar.tsx b/lib/src/DateRangePicker/DateRangePickerToolbar.tsx new file mode 100644 index 000000000..2dae52594 --- /dev/null +++ b/lib/src/DateRangePicker/DateRangePickerToolbar.tsx @@ -0,0 +1,83 @@ +import * as React from 'react'; +import PickerToolbar from '../_shared/PickerToolbar'; +import Typography from '@material-ui/core/Typography'; +import { useUtils } from '../_shared/hooks/useUtils'; +import { makeStyles } from '@material-ui/core/styles'; +import { ToolbarComponentProps } from '../Picker/Picker'; +import { ToolbarButton } from '../_shared/ToolbarButton'; +import { DateRange, CurrentlySelectingRangeEndProps } from './RangeTypes'; + +export const useStyles = makeStyles( + { + penIcon: { + position: 'relative', + top: 4, + }, + dateTextContainer: { + display: 'flex', + }, + }, + { name: 'MuiPickersDatePickerRoot' } +); + +interface DateRangePickerToolbarProps + extends CurrentlySelectingRangeEndProps, + Pick< + ToolbarComponentProps, + 'isMobileKeyboardViewOpen' | 'toggleMobileKeyboardView' | 'toolbarTitle' | 'toolbarFormat' + > { + date: DateRange; + startText: React.ReactNode; + endText: React.ReactNode; + currentlySelectingRangeEnd: 'start' | 'end'; + setCurrentlySelectingRangeEnd: (newSelectingEnd: 'start' | 'end') => void; +} + +export const DateRangePickerToolbar: React.FC = ({ + date: [start, end], + toolbarFormat, + isMobileKeyboardViewOpen, + toggleMobileKeyboardView, + currentlySelectingRangeEnd, + setCurrentlySelectingRangeEnd, + startText, + endText, + toolbarTitle = 'SELECT DATE RANGE', +}) => { + const utils = useUtils(); + const classes = useStyles(); + + const startDateValue = start + ? utils.formatByString(start, toolbarFormat || utils.formats.shortDate) + : startText; + + const endDateValue = end + ? utils.formatByString(end, toolbarFormat || utils.formats.shortDate) + : endText; + + return ( + +
+ setCurrentlySelectingRangeEnd('start')} + /> +  {'–'}  + setCurrentlySelectingRangeEnd('end')} + /> +
+
+ ); +}; diff --git a/lib/src/DateRangePicker/DateRangePickerView.tsx b/lib/src/DateRangePicker/DateRangePickerView.tsx new file mode 100644 index 000000000..b23065be5 --- /dev/null +++ b/lib/src/DateRangePicker/DateRangePickerView.tsx @@ -0,0 +1,216 @@ +import * as React from 'react'; +import { isRangeValid } from '../_helpers/date-utils'; +import { MaterialUiPickersDate } from '../typings/date'; +import { BasePickerProps } from '../typings/BasePicker'; +import { calculateRangeChange } from './date-range-manager'; +import { useUtils, useNow } from '../_shared/hooks/useUtils'; +import { DateRangePickerInput } from './DateRangePickerInput'; +import { SharedPickerProps } from '../Picker/SharedPickerProps'; +import { DateRangePickerToolbar } from './DateRangePickerToolbar'; +import { useParsedDate } from '../_shared/hooks/date-helpers-hooks'; +import { useCalendarState } from '../views/Calendar/useCalendarState'; +import { FORCE_FINISH_PICKER } from '../_shared/hooks/usePickerState'; +import { DateRangePickerViewMobile } from './DateRangePickerViewMobile'; +import { WrapperVariantContext } from '../wrappers/WrapperVariantContext'; +import { MobileKeyboardInputView } from '../views/MobileKeyboardInputView'; +import { RangeInput, DateRange, CurrentlySelectingRangeEndProps } from './RangeTypes'; +import { ExportedCalendarViewProps, defaultReduceAnimations } from '../views/Calendar/CalendarView'; +import { + DateRangePickerViewDesktop, + ExportedDesktopDateRangeCalendarProps, +} from './DateRangePickerViewDesktop'; + +type BaseCalendarPropsToReuse = Omit; + +export interface ExportedDateRangePickerViewProps + extends BaseCalendarPropsToReuse, + ExportedDesktopDateRangeCalendarProps, + Omit { + /** + * if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date + * @default false + */ + disableAutoMonthSwitching?: boolean; +} + +interface DateRangePickerViewProps + extends ExportedDateRangePickerViewProps, + CurrentlySelectingRangeEndProps, + SharedPickerProps { + open: boolean; + startText: React.ReactNode; + endText: React.ReactNode; +} + +export const DateRangePickerView: React.FC = ({ + open, + calendars = 2, + currentlySelectingRangeEnd, + date, + disableAutoMonthSwitching = false, + disableFuture, + disableHighlightToday, + disablePast, + maxDate: unparsedMaxDate = new Date('2100-01-01'), + minDate: unparsedMinDate = new Date('1900-01-01'), + onDateChange, + onMonthChange, + reduceAnimations = defaultReduceAnimations, + setCurrentlySelectingRangeEnd, + shouldDisableDate, + toggleMobileKeyboardView, + isMobileKeyboardViewOpen, + showToolbar, + startText, + endText, + DateInputProps, + ...other +}) => { + const now = useNow(); + const utils = useUtils(); + const wrapperVariant = React.useContext(WrapperVariantContext); + const minDate = useParsedDate(unparsedMinDate)!; + const maxDate = useParsedDate(unparsedMaxDate)!; + + const [start, end] = date; + const { + changeMonth, + calendarState, + isDateDisabled, + onMonthSwitchingAnimationEnd, + changeFocusedDay, + } = useCalendarState({ + date: start || end || now, + minDate, + maxDate, + reduceAnimations, + disablePast, + disableFuture, + onMonthChange, + shouldDisableDate, + disableSwitchToMonthOnDayFocus: true, + }); + + const toShowToolbar = showToolbar ?? wrapperVariant !== 'desktop'; + + const scrollToDayIfNeeded = (day: MaterialUiPickersDate) => { + const displayingMonthRange = wrapperVariant === 'mobile' ? 0 : calendars - 1; + const currentMonthNumber = utils.getMonth(calendarState.currentMonth); + const requestedMonthNumber = utils.getMonth(day); + + if ( + requestedMonthNumber < currentMonthNumber || + requestedMonthNumber > currentMonthNumber + displayingMonthRange + ) { + const newMonth = + currentlySelectingRangeEnd === 'start' + ? start + : // If need to focus end, scroll to the state when "end" is displaying in the last calendar + utils.addMonths(end, -displayingMonthRange); + + changeMonth(newMonth); + } + }; + + React.useEffect(() => { + if (disableAutoMonthSwitching || !open) { + return; + } + + if ( + (currentlySelectingRangeEnd === 'start' && start === null) || + (currentlySelectingRangeEnd === 'end' && end === null) + ) { + return; + } + + scrollToDayIfNeeded(currentlySelectingRangeEnd === 'start' ? start : end); + }, [currentlySelectingRangeEnd, date]); // eslint-disable-line + + const handleChange = React.useCallback( + (newDate: MaterialUiPickersDate) => { + const { nextSelection, newRange } = calculateRangeChange({ + newDate, + utils, + range: date, + currentlySelectingRangeEnd, + }); + + setCurrentlySelectingRangeEnd(nextSelection); + + const isFullRangeSelected = + currentlySelectingRangeEnd === 'end' && isRangeValid(utils, newRange); + onDateChange(newRange, wrapperVariant, isFullRangeSelected ? FORCE_FINISH_PICKER : true); + }, + [ + currentlySelectingRangeEnd, + date, + onDateChange, + setCurrentlySelectingRangeEnd, + utils, + wrapperVariant, + ] + ); + + const renderView = () => { + const sharedCalendarProps = { + date, + isDateDisabled, + changeFocusedDay, + onChange: handleChange, + reduceAnimations, + disableHighlightToday, + onMonthSwitchingAnimationEnd, + changeMonth, + currentlySelectingRangeEnd, + disableFuture, + disablePast, + minDate, + maxDate, + ...calendarState, + ...other, + }; + + switch (wrapperVariant) { + case 'desktop': { + return ; + } + + default: { + return ; + } + } + }; + + return ( + <> + {toShowToolbar && ( + + )} + + {isMobileKeyboardViewOpen ? ( + + + + ) : ( + renderView() + )} + + ); +}; diff --git a/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx b/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx new file mode 100644 index 000000000..f3e0da160 --- /dev/null +++ b/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx @@ -0,0 +1,190 @@ +import * as React from 'react'; +import { DateRange } from './RangeTypes'; +import { DateRangeDay } from './DateRangePickerDay'; +import { useUtils } from '../_shared/hooks/useUtils'; +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 { ArrowSwitcher, ExportedArrowSwitcherProps } from '../_shared/ArrowSwitcher'; +import { + usePreviousMonthDisabled, + useNextMonthDisabled, +} from '../_shared/hooks/date-helpers-hooks'; + +export interface ExportedDesktopDateRangeCalendarProps { + /** + * How many calendars render on **desktop** DateRangePicker + * @default 2 + */ + calendars?: 1 | 2 | 3; +} + +interface DesktopDateRangeCalendarProps + extends ExportedDesktopDateRangeCalendarProps, + CalendarProps, + ExportedArrowSwitcherProps { + date: DateRange; + changeMonth: (date: MaterialUiPickersDate) => void; + currentlySelectingRangeEnd: 'start' | 'end'; +} + +export const useStyles = makeStyles( + theme => ({ + dateRangeContainer: { + display: 'flex', + flexDirection: 'row', + }, + rangeCalendarContainer: { + '&:not(:last-child)': { + borderRight: `2px solid ${theme.palette.divider}`, + }, + }, + calendar: { + minWidth: 312, + minHeight: 288, + }, + arrowSwitcher: { + padding: '16px 16px 8px 16px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + }, + }), + { name: 'MuiPickersDesktopDateRangeCalendar' } +); + +function getCalendarsArray(calendars: ExportedDesktopDateRangeCalendarProps['calendars']) { + switch (calendars) { + case 1: + return [0]; + case 2: + return [0, 0]; + case 3: + return [0, 0, 0]; + // this will not work in IE11, but allows to support any amount of calendars + default: + return new Array(calendars).fill(0); + } +} + +export const DateRangePickerViewDesktop: React.FC = ({ + date, + calendars = 2, + changeMonth, + leftArrowButtonProps, + leftArrowButtonText, + leftArrowIcon, + rightArrowButtonProps, + rightArrowButtonText, + rightArrowIcon, + onChange, + disableFuture, + disablePast, + minDate, + maxDate, + currentlySelectingRangeEnd, + currentMonth, + ...other +}) => { + const utils = useUtils(); + const classes = useStyles(); + const [rangePreviewDay, setRangePreviewDay] = React.useState(null); + + const isNextMonthDisabled = useNextMonthDisabled(currentMonth, { disableFuture, maxDate }); + const isPreviousMonthDisabled = usePreviousMonthDisabled(currentMonth, { disablePast, minDate }); + + const previewingRange = calculateRangePreview({ + utils, + range: date, + newDate: rangePreviewDay, + currentlySelectingRangeEnd, + }); + + const handleDayChange = React.useCallback( + (day: MaterialUiPickersDate) => { + setRangePreviewDay(null); + onChange(day); + }, + [onChange] + ); + + const handlePreviewDayChange = (newPreviewRequest: MaterialUiPickersDate) => { + if (!isWithinRange(utils, newPreviewRequest, date)) { + setRangePreviewDay(newPreviewRequest); + } else { + setRangePreviewDay(null); + } + }; + + const CalendarTransitionProps = React.useMemo( + () => ({ + onMouseLeave: () => setRangePreviewDay(null), + }), + [] + ); + + const selectNextMonth = React.useCallback(() => { + changeMonth(utils.getNextMonth(currentMonth)); + }, [changeMonth, currentMonth, utils]); + + const selectPreviousMonth = React.useCallback(() => { + changeMonth(utils.getPreviousMonth(currentMonth)); + }, [changeMonth, currentMonth, utils]); + + return ( +
+ {getCalendarsArray(calendars).map((_, index) => { + const monthOnIteration = utils.setMonth(currentMonth, utils.getMonth(currentMonth) + index); + + return ( +
+ + + ( + handlePreviewDayChange(day)} + {...DayProps} + /> + )} + /> +
+ ); + })} +
+ ); +}; diff --git a/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx b/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx new file mode 100644 index 000000000..ca6da5cf9 --- /dev/null +++ b/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import CalendarHeader from '../views/Calendar/CalendarHeader'; +import { DateRange } from './RangeTypes'; +import { DateRangeDay } from './DateRangePickerDay'; +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'; + +export interface ExportedMobileDateRangeCalendarProps {} + +interface DesktopDateRangeCalendarProps + extends ExportedMobileDateRangeCalendarProps, + CalendarProps, + ExportedArrowSwitcherProps { + date: DateRange; + changeMonth: (date: MaterialUiPickersDate) => void; +} + +const onlyDateView = ['date'] as ['date']; + +export const DateRangePickerViewMobile: React.FC = ({ + date, + changeMonth, + leftArrowButtonProps, + leftArrowButtonText, + leftArrowIcon, + rightArrowButtonProps, + rightArrowButtonText, + rightArrowIcon, + onChange, + ...other +}) => { + const utils = useUtils(); + + return ( + <> + ({})} + onMonthChange={changeMonth} + leftArrowButtonProps={leftArrowButtonProps} + leftArrowButtonText={leftArrowButtonText} + leftArrowIcon={leftArrowIcon} + rightArrowButtonProps={rightArrowButtonProps} + rightArrowButtonText={rightArrowButtonText} + rightArrowIcon={rightArrowIcon} + {...other} + /> + + ( + + )} + /> + + ); +}; diff --git a/lib/src/DateRangePicker/RangeTypes.ts b/lib/src/DateRangePicker/RangeTypes.ts new file mode 100644 index 000000000..5a39025e3 --- /dev/null +++ b/lib/src/DateRangePicker/RangeTypes.ts @@ -0,0 +1,10 @@ +import { ParsableDate } from '../constants/prop-types'; +import { MaterialUiPickersDate } from '../typings/date'; + +export type RangeInput = [ParsableDate, ParsableDate]; +export type DateRange = [MaterialUiPickersDate, MaterialUiPickersDate]; + +export interface CurrentlySelectingRangeEndProps { + currentlySelectingRangeEnd: 'start' | 'end'; + setCurrentlySelectingRangeEnd: (newSelectingEnd: 'start' | 'end') => void; +} diff --git a/lib/src/DateRangePicker/date-range-manager.ts b/lib/src/DateRangePicker/date-range-manager.ts new file mode 100644 index 000000000..689b59527 --- /dev/null +++ b/lib/src/DateRangePicker/date-range-manager.ts @@ -0,0 +1,48 @@ +import { DateRange } from './DateRangePicker'; +import { MaterialUiPickersDate } from '../typings/date'; +import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; + +interface CalculateRangeChangeOptions { + utils: MuiPickersAdapter; + range: DateRange; + newDate: MaterialUiPickersDate; + currentlySelectingRangeEnd: 'start' | 'end'; +} + +export function calculateRangeChange({ + utils, + range, + newDate: selectedDate, + currentlySelectingRangeEnd, +}: CalculateRangeChangeOptions): { nextSelection: 'start' | 'end'; newRange: DateRange } { + const [start, end] = range; + + if (currentlySelectingRangeEnd === 'start') { + return Boolean(end) && utils.isAfter(selectedDate, end) + ? { nextSelection: 'end', newRange: [selectedDate, null] } + : { nextSelection: 'end', newRange: [selectedDate, end] }; + } else { + return Boolean(start) && utils.isBefore(selectedDate, start) + ? { nextSelection: 'end', newRange: [selectedDate, null] } + : { nextSelection: 'start', newRange: [start, selectedDate] }; + } +} + +export function calculateRangePreview(options: CalculateRangeChangeOptions): DateRange { + if (!options.newDate) { + return [null, null]; + } + + const [start, end] = options.range; + const { newRange } = calculateRangeChange(options); + + if (!start || !end) { + return newRange; + } + + const [previewStart, previewEnd] = newRange; + // prettier-ignore + return options.currentlySelectingRangeEnd === 'end' + ? [end, previewEnd] + : [previewStart, start]; +} diff --git a/lib/src/DateTimePicker/DateTimePickerToolbar.tsx b/lib/src/DateTimePicker/DateTimePickerToolbar.tsx index 610970468..0631b3e00 100644 --- a/lib/src/DateTimePicker/DateTimePickerToolbar.tsx +++ b/lib/src/DateTimePicker/DateTimePickerToolbar.tsx @@ -70,7 +70,7 @@ export const DateTimePickerToolbar: React.FC = ({ variant="subtitle1" onClick={() => setOpenView('year')} selected={openView === 'year'} - label={utils.format(date, 'year')} + value={utils.format(date, 'year')} /> = ({ data-mui-test="datetimepicker-toolbar-date" onClick={() => setOpenView('date')} selected={openView === 'date'} - label={ + value={ toolbarFormat ? utils.formatByString(date, toolbarFormat) : utils.format(date, 'shortDate') @@ -93,18 +93,18 @@ export const DateTimePickerToolbar: React.FC = ({ variant="h3" onClick={() => setOpenView('hours')} selected={openView === 'hours'} - label={ampm ? utils.format(date, 'hours12h') : utils.format(date, 'hours24h')} + value={ampm ? utils.format(date, 'hours12h') : utils.format(date, 'hours24h')} typographyClassName={classes.timeTypography} /> - + setOpenView('minutes')} selected={openView === 'minutes'} - label={utils.format(date, 'minutes')} + value={utils.format(date, 'minutes')} typographyClassName={classes.timeTypography} /> diff --git a/lib/src/Picker/Picker.tsx b/lib/src/Picker/Picker.tsx index 558d01728..a4d9feacc 100644 --- a/lib/src/Picker/Picker.tsx +++ b/lib/src/Picker/Picker.tsx @@ -1,32 +1,32 @@ import * as React from 'react'; import clsx from 'clsx'; -import { WrapperVariant } from '../wrappers/Wrapper'; +import KeyboardDateInput from '../_shared/KeyboardDateInput'; import { useViews } from '../_shared/hooks/useViews'; import { makeStyles } from '@material-ui/core/styles'; import { DateTimePickerView } from '../DateTimePicker'; import { ParsableDate } from '../constants/prop-types'; import { BasePickerProps } from '../typings/BasePicker'; import { MaterialUiPickersDate } from '../typings/date'; -import { DateInputProps } from '../_shared/PureDateInput'; import { DatePickerView } from '../DatePicker/DatePicker'; import { useIsLandscape } from '../_shared/hooks/useIsLandscape'; -import { WithViewsProps, AnyPickerView } from './SharedPickerProps'; import { DIALOG_WIDTH, VIEW_HEIGHT } from '../constants/dimensions'; import { WrapperVariantContext } from '../wrappers/WrapperVariantContext'; import { MobileKeyboardInputView } from '../views/MobileKeyboardInputView'; import { ClockView, ExportedClockViewProps } from '../views/Clock/ClockView'; +import { WithViewsProps, AnyPickerView, SharedPickerProps } from './SharedPickerProps'; import { CalendarView, ExportedCalendarViewProps } from '../views/Calendar/CalendarView'; type CalendarAndClockProps = ExportedCalendarViewProps & ExportedClockViewProps; export type ToolbarComponentProps< - T extends AnyPickerView = AnyPickerView + TDate = MaterialUiPickersDate, + TView extends AnyPickerView = AnyPickerView > = CalendarAndClockProps & { - views: T[]; - openView: T; - date: MaterialUiPickersDate; - setOpenView: (view: T) => void; - onChange: (date: MaterialUiPickersDate, isFinish?: boolean) => void; + views: TView[]; + openView: TView; + date: TDate; + setOpenView: (view: TView) => void; + onChange: (date: TDate, isFinish?: boolean) => void; toolbarTitle?: React.ReactNode; toolbarFormat?: string; // TODO move out, cause it is DateTimePickerOnly @@ -50,21 +50,11 @@ export interface ExportedPickerProps timeIcon?: React.ReactNode; } -export interface PickerProps< +export type PickerProps< TView extends AnyPickerView, TInputValue = ParsableDate, TDateValue = MaterialUiPickersDate -> extends ExportedPickerProps { - isMobileKeyboardViewOpen: boolean; - toggleMobileKeyboardView: () => void; - DateInputProps: DateInputProps; - date: TDateValue | null; - onDateChange: ( - date: TDateValue, - currentVariant: WrapperVariant, - isFinish?: boolean | symbol - ) => void; -} +> = ExportedPickerProps & SharedPickerProps; export const useStyles = makeStyles( { @@ -157,7 +147,14 @@ export function Picker({ })} > {isMobileKeyboardViewOpen ? ( - + + + ) : ( <> {(openView === 'year' || openView === 'month' || openView === 'date') && ( diff --git a/lib/src/Picker/SharedPickerProps.tsx b/lib/src/Picker/SharedPickerProps.tsx index 53b57f92b..1ae3d8db2 100644 --- a/lib/src/Picker/SharedPickerProps.tsx +++ b/lib/src/Picker/SharedPickerProps.tsx @@ -1,16 +1,34 @@ +import { WrapperVariant } from '../wrappers/Wrapper'; import { DateTimePickerView } from '../DateTimePicker'; +import { ParsableDate } from '../constants/prop-types'; import { BasePickerProps } from '../typings/BasePicker'; -import { ExportedDateInputProps } from '../_shared/PureDateInput'; +import { MaterialUiPickersDate } from '../typings/date'; import { DateValidationProps } from '../_helpers/text-field-helper'; import { WithDateAdapterProps } from '../_shared/withDateAdapterProp'; +import { ExportedDateInputProps, DateInputProps } from '../_shared/PureDateInput'; export type AnyPickerView = DateTimePickerView; -export type AllSharedPickerProps = WithDateAdapterProps & - BasePickerProps & - ExportedDateInputProps & +export type AllSharedPickerProps< + TInputValue = ParsableDate, + TDateValue = MaterialUiPickersDate +> = BasePickerProps & + ExportedDateInputProps & + WithDateAdapterProps & DateValidationProps; +export interface SharedPickerProps { + isMobileKeyboardViewOpen: boolean; + toggleMobileKeyboardView: () => void; + DateInputProps: DateInputProps; + date: TDateValue; + onDateChange: ( + date: TDateValue, + currentVariant: WrapperVariant, + isFinish?: boolean | symbol + ) => void; +} + export interface WithViewsProps { /** * Array of views to show @@ -19,5 +37,3 @@ export interface WithViewsProps { /** First view to show */ openTo?: T; } - -export type WithDateInputProps = DateValidationProps & BasePickerProps & ExportedDateInputProps; diff --git a/lib/src/Picker/makePickerWithState.tsx b/lib/src/Picker/makePickerWithState.tsx index 728b64864..5f2548934 100644 --- a/lib/src/Picker/makePickerWithState.tsx +++ b/lib/src/Picker/makePickerWithState.tsx @@ -1,15 +1,16 @@ import * as React from 'react'; +import { useUtils } from '../_shared/hooks/useUtils'; import { ParsableDate } from '../constants/prop-types'; import { MaterialUiPickersDate } from '../typings/date'; -import { PureDateInput } from '../_shared/PureDateInput'; import { parsePickerInputValue } from '../_helpers/date-utils'; +import { SomeWrapper, ExtendWrapper } from '../wrappers/Wrapper'; import { KeyboardDateInput } from '../_shared/KeyboardDateInput'; import { usePickerState } from '../_shared/hooks/usePickerState'; -import { SomeWrapper, ExtendWrapper } from '../wrappers/Wrapper'; import { validateDateValue } from '../_helpers/text-field-helper'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; import { withDateAdapterProp } from '../_shared/withDateAdapterProp'; import { makeWrapperComponent } from '../wrappers/makeWrapperComponent'; +import { PureDateInput, DateInputProps } from '../_shared/PureDateInput'; import { AnyPickerView, AllSharedPickerProps } from './SharedPickerProps'; import { Picker, ToolbarComponentProps, ExportedPickerProps } from './Picker'; @@ -24,19 +25,28 @@ export function makePickerWithStateAndWrapper< T extends AllAvailableForOverrideProps, TWrapper extends SomeWrapper = typeof ResponsiveWrapper >(Wrapper: TWrapper, { useDefaultProps, DefaultToolbarComponent }: MakePickerOptions) { - const PickerWrapper = makeWrapperComponent(Wrapper, { - KeyboardDateInputComponent: KeyboardDateInput, - PureDateInputComponent: PureDateInput, - }); + const PickerWrapper = makeWrapperComponent( + Wrapper, + { + KeyboardDateInputComponent: KeyboardDateInput, + PureDateInputComponent: PureDateInput, + } + ); function PickerWithState(props: T & AllSharedPickerProps & ExtendWrapper) { + const utils = useUtils(); const defaultProps = useDefaultProps(props); const allProps = { ...defaultProps, ...props }; const { pickerProps, inputProps, wrapperProps } = usePickerState< ParsableDate, MaterialUiPickersDate - >(allProps, parsePickerInputValue, validateDateValue); + >(allProps, { + emptyValue: null, + parseInput: parsePickerInputValue, + validateInput: validateDateValue, + areValuesEqual: (a, b) => utils.isEqual(a, b), + }); const { allowKeyboardControl, @@ -44,14 +54,22 @@ export function makePickerWithStateAndWrapper< ampmInClock, dateRangeIcon, disableFuture, + disableHighlightToday, disablePast, - showToolbar, + disableTimeValidationIgnoreDatePart, hideTabs, leftArrowButtonProps, + leftArrowButtonText, leftArrowIcon, loadingIndicator, maxDate, + maxTime, minDate, + // @ts-ignore Especial DateTimePicker only prop that are needed only on the upper level + minDateTime, + // @ts-ignore Especial DateTimePicker only prop that are needed only on the upper level + maxDateTime, + minTime, minutesStep, onMonthChange, onYearChange, @@ -59,19 +77,18 @@ export function makePickerWithStateAndWrapper< orientation, renderDay, rightArrowButtonProps, + rightArrowButtonText, rightArrowIcon, shouldDisableDate, shouldDisableTime, + showDaysOutsideCurrentMonth, + showToolbar, timeIcon, - toolbarFormat, ToolbarComponent = DefaultToolbarComponent, - views, + toolbarFormat, toolbarTitle, - disableTimeValidationIgnoreDatePart, - showDaysOutsideCurrentMonth, - disableHighlightToday, - minTime, - maxTime, + value, + views, ...restPropsForTextField } = allProps; @@ -91,6 +108,7 @@ export function makePickerWithStateAndWrapper< hideTabs={hideTabs} leftArrowButtonProps={leftArrowButtonProps} leftArrowIcon={leftArrowIcon} + leftArrowButtonText={leftArrowButtonText} loadingIndicator={loadingIndicator} maxDate={maxDate} maxTime={maxTime} @@ -104,13 +122,14 @@ export function makePickerWithStateAndWrapper< renderDay={renderDay} rightArrowButtonProps={rightArrowButtonProps} rightArrowIcon={rightArrowIcon} + rightArrowButtonText={rightArrowButtonText} shouldDisableDate={shouldDisableDate} shouldDisableTime={shouldDisableTime} showDaysOutsideCurrentMonth={showDaysOutsideCurrentMonth} showToolbar={showToolbar} timeIcon={timeIcon} - toolbarFormat={toolbarFormat} ToolbarComponent={ToolbarComponent} + toolbarFormat={toolbarFormat} toolbarTitle={toolbarTitle || restPropsForTextField?.label} views={views} /> diff --git a/lib/src/TimePicker/TimePickerToolbar.tsx b/lib/src/TimePicker/TimePickerToolbar.tsx index 0ba17e581..9162ee0c3 100644 --- a/lib/src/TimePicker/TimePickerToolbar.tsx +++ b/lib/src/TimePicker/TimePickerToolbar.tsx @@ -93,7 +93,7 @@ export const TimePickerToolbar: React.FC = ({ const separator = ( = ({ variant={clockTypographyVariant} onClick={() => setOpenView('hours')} selected={openView === 'hours'} - label={ampm ? utils.format(date, 'hours12h') : utils.format(date, 'hours24h')} + value={ampm ? utils.format(date, 'hours12h') : utils.format(date, 'hours24h')} /> )} @@ -135,7 +135,7 @@ export const TimePickerToolbar: React.FC = ({ variant={clockTypographyVariant} onClick={() => setOpenView('minutes')} selected={openView === 'minutes'} - label={utils.format(date, 'minutes')} + value={utils.format(date, 'minutes')} /> )} @@ -147,7 +147,7 @@ export const TimePickerToolbar: React.FC = ({ variant={clockTypographyVariant} onClick={() => setOpenView('seconds')} selected={openView === 'seconds'} - label={utils.format(date, 'seconds')} + value={utils.format(date, 'seconds')} /> )} @@ -164,7 +164,7 @@ export const TimePickerToolbar: React.FC = ({ data-mui-test="toolbar-am-btn" selected={meridiemMode === 'am'} typographyClassName={classes.ampmLabel} - label={utils.getMeridiemText('am')} + value={utils.getMeridiemText('am')} onClick={() => handleMeridiemChange('am')} /> @@ -174,7 +174,7 @@ export const TimePickerToolbar: React.FC = ({ data-mui-test="toolbar-pm-btn" selected={meridiemMode === 'pm'} typographyClassName={classes.ampmLabel} - label={utils.getMeridiemText('pm')} + value={utils.getMeridiemText('pm')} onClick={() => handleMeridiemChange('pm')} /> diff --git a/lib/src/__tests__/e2e/DatePicker.test.tsx b/lib/src/__tests__/DatePicker.test.tsx similarity index 97% rename from lib/src/__tests__/e2e/DatePicker.test.tsx rename to lib/src/__tests__/DatePicker.test.tsx index 0df80067d..2012433be 100644 --- a/lib/src/__tests__/e2e/DatePicker.test.tsx +++ b/lib/src/__tests__/DatePicker.test.tsx @@ -1,14 +1,13 @@ import * as React from 'react'; -// import { act } from 'react-dom' import { ReactWrapper } from 'enzyme'; -import { Picker } from '../../Picker/Picker'; -import { mount, utilsToUse } from '../test-utils'; +import { Picker } from '../Picker/Picker'; +import { mount, utilsToUse } from './test-utils'; import { DatePicker, MobileDatePicker, DesktopDatePicker, DatePickerProps, -} from '../../DatePicker/DatePicker'; +} from '../DatePicker/DatePicker'; describe('e2e - DatePicker default year format', () => { let component: ReactWrapper; diff --git a/lib/src/__tests__/e2e/DatePickerProps.test.tsx b/lib/src/__tests__/DatePickerProps.test.tsx similarity index 95% rename from lib/src/__tests__/e2e/DatePickerProps.test.tsx rename to lib/src/__tests__/DatePickerProps.test.tsx index 7a7afa640..a17b3d4f0 100644 --- a/lib/src/__tests__/e2e/DatePickerProps.test.tsx +++ b/lib/src/__tests__/DatePickerProps.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { mount, utilsToUse } from '../test-utils'; -import { DatePicker, MobileDatePicker } from '../../DatePicker/DatePicker'; +import { mount, utilsToUse } from './test-utils'; +import { DatePicker, MobileDatePicker } from '../DatePicker/DatePicker'; describe('DatePicker - different props', () => { it('Should not render toolbar if onlyCalendar = true', () => { diff --git a/lib/src/__tests__/e2e/DatePickerRoot.test.tsx b/lib/src/__tests__/DatePickerRoot.test.tsx similarity index 96% rename from lib/src/__tests__/e2e/DatePickerRoot.test.tsx rename to lib/src/__tests__/DatePickerRoot.test.tsx index 3e96635e3..a7b04d268 100644 --- a/lib/src/__tests__/e2e/DatePickerRoot.test.tsx +++ b/lib/src/__tests__/DatePickerRoot.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { ReactWrapper } from 'enzyme'; import { clickOKButton } from './commands'; -import { mount, utilsToUse } from '../test-utils'; -import { DatePicker, DatePickerProps } from '../../DatePicker'; +import { mount, utilsToUse } from './test-utils'; +import { DatePicker, DatePickerProps } from '../DatePicker'; describe('e2e - DatePicker', () => { let component: ReactWrapper; diff --git a/lib/src/__tests__/DateRangePicker.test.tsx b/lib/src/__tests__/DateRangePicker.test.tsx new file mode 100644 index 000000000..edb22030f --- /dev/null +++ b/lib/src/__tests__/DateRangePicker.test.tsx @@ -0,0 +1,87 @@ +// 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 { mount, utilsToUse } from './test-utils'; +import { DesktopDateRangePicker } from '../DateRangePicker/DateRangePicker'; + +describe('DateRangePicker', () => { + test('allows select range', () => { + const component = mount( + + ); + + expect(component.find('[data-mui-test="DateRangeHighlight"]').length).toBe(31); + }); + + test('allows disabling dates', () => { + const component = mount( + isWeekend(utilsToUse.toJsDate(date))} + onChange={jest.fn()} + value={[ + utilsToUse.date('2018-01-01T00:00:00.000'), + utilsToUse.date('2018-01-31T00:00:00.000'), + ]} + /> + ); + + expect( + component + .find('button[data-mui-test="DateRangeDay"]') + .filterWhere(wrapper => !wrapper.prop('disabled')).length + ).toBe(70); + }); + + test('forwardRef', () => { + const Component = () => { + const ref = React.useRef(null); + + React.useEffect(() => { + expect(ref?.current?.id).toBe('test-ref'); + expect(ref?.current).toBeInstanceOf(HTMLDivElement); + }); + + return ( + + ); + }; + + mount(); + }); + + test('prop: calendars', () => { + const component = mount( + + ); + + expect(component.find('Calendar').length).toBe(3); + expect(component.find('button[data-mui-test="DateRangeDay"]').length).toBe(105); + }); +}); diff --git a/lib/src/__tests__/e2e/DateTimePicker.test.tsx b/lib/src/__tests__/DateTimePicker.test.tsx similarity index 94% rename from lib/src/__tests__/e2e/DateTimePicker.test.tsx rename to lib/src/__tests__/DateTimePicker.test.tsx index 8b8044870..c1b59358e 100644 --- a/lib/src/__tests__/e2e/DateTimePicker.test.tsx +++ b/lib/src/__tests__/DateTimePicker.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { ReactWrapper } from 'enzyme'; -import { mount, utilsToUse } from '../test-utils'; +import { mount, utilsToUse } from './test-utils'; import { mount as enzymeDefaultMount } from 'enzyme'; import { ThemeProvider, createMuiTheme } from '@material-ui/core'; -import { DateTimePicker, DateTimePickerProps } from '../../DateTimePicker/DateTimePicker'; +import { DateTimePicker, DateTimePickerProps } from '../DateTimePicker/DateTimePicker'; const format = process.env.UTILS === 'moment' ? 'MM/DD/YYYY HH:mm' : 'MM/dd/yyyy hh:mm'; diff --git a/lib/src/__tests__/e2e/DateTimePickerRoot.test.tsx b/lib/src/__tests__/DateTimePickerRoot.test.tsx similarity index 94% rename from lib/src/__tests__/e2e/DateTimePickerRoot.test.tsx rename to lib/src/__tests__/DateTimePickerRoot.test.tsx index 34e4592c0..211dfbcbb 100644 --- a/lib/src/__tests__/e2e/DateTimePickerRoot.test.tsx +++ b/lib/src/__tests__/DateTimePickerRoot.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { ReactWrapper } from 'enzyme'; import { clickOKButton } from './commands'; -import { mount, utilsToUse, toHaveBeenCalledExceptMoment } from '../test-utils'; -import { MobileDateTimePicker, DateTimePickerProps } from '../../DateTimePicker/DateTimePicker'; +import { mount, utilsToUse, toHaveBeenCalledExceptMoment } from './test-utils'; +import { MobileDateTimePicker, DateTimePickerProps } from '../DateTimePicker/DateTimePicker'; describe('e2e - DateTimePicker', () => { let component: ReactWrapper; diff --git a/lib/src/__tests__/e2e/KeyboardDatePicker.test.tsx b/lib/src/__tests__/KeyboardDatePicker.test.tsx similarity index 97% rename from lib/src/__tests__/e2e/KeyboardDatePicker.test.tsx rename to lib/src/__tests__/KeyboardDatePicker.test.tsx index a690aeada..0eef560c4 100644 --- a/lib/src/__tests__/e2e/KeyboardDatePicker.test.tsx +++ b/lib/src/__tests__/KeyboardDatePicker.test.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import addDays from 'date-fns/addDays'; +import { mount } from './test-utils'; import { ReactWrapper } from 'enzyme'; -import { mount } from '../test-utils'; -import { DesktopDatePicker, DatePickerProps } from '../../DatePicker/DatePicker'; +import { DesktopDatePicker, DatePickerProps } from '../DatePicker/DatePicker'; describe('e2e -- DatePicker keyboard input', () => { const onChangeMock = jest.fn(); diff --git a/lib/src/__tests__/e2e/KeyboardDateTimePicker.test.tsx b/lib/src/__tests__/KeyboardDateTimePicker.test.tsx similarity index 89% rename from lib/src/__tests__/e2e/KeyboardDateTimePicker.test.tsx rename to lib/src/__tests__/KeyboardDateTimePicker.test.tsx index 7911b2f8e..de300b848 100644 --- a/lib/src/__tests__/e2e/KeyboardDateTimePicker.test.tsx +++ b/lib/src/__tests__/KeyboardDateTimePicker.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ReactWrapper } from 'enzyme'; -import { mount, utilsToUse } from '../test-utils'; -import { DesktopDateTimePicker, DateTimePickerProps } from '../../DateTimePicker/DateTimePicker'; +import { mount, utilsToUse } from './test-utils'; +import { DesktopDateTimePicker, DateTimePickerProps } from '../DateTimePicker/DateTimePicker'; const format = process.env.UTILS === 'moment' ? 'MM/DD/YYYY HH:mm' : 'MM/dd/yyyy hh:mm'; diff --git a/lib/src/__tests__/MuiPickersUtilsProvider.test.tsx b/lib/src/__tests__/MuiPickersUtilsProvider.test.tsx deleted file mode 100644 index 3daec6844..000000000 --- a/lib/src/__tests__/MuiPickersUtilsProvider.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// required to use just shallow here because utils prop override -import * as React from 'react'; -import DateFnsUtils from '@date-io/date-fns'; -import LocalizationProvider, { LocalizationProviderProps } from '../LocalizationProvider'; -import { shallow, ShallowWrapper } from 'enzyme'; // required to use just shallow here because utils prop override - -describe('LocalizationProvider', () => { - let component: ShallowWrapper; - - beforeEach(() => { - component = shallow( - -
- - ); - }); - - it('Should render context provider', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/lib/src/__tests__/e2e/Theme.test.tsx b/lib/src/__tests__/Theme.test.tsx similarity index 81% rename from lib/src/__tests__/e2e/Theme.test.tsx rename to lib/src/__tests__/Theme.test.tsx index ff9c77efe..1d8538a50 100644 --- a/lib/src/__tests__/e2e/Theme.test.tsx +++ b/lib/src/__tests__/Theme.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { mount } from '../test-utils'; -import { DatePicker } from '../../DatePicker'; +import { mount } from './test-utils'; +import { DatePicker } from '../DatePicker'; import { ThemeProvider } from '@material-ui/core/styles'; import { createMuiTheme } from '@material-ui/core/styles'; -import { DateTimePicker } from '../../DateTimePicker/DateTimePicker'; +import { DateTimePicker } from '../DateTimePicker/DateTimePicker'; const theme = createMuiTheme({ palette: { diff --git a/lib/src/__tests__/e2e/TimePicker.test.tsx b/lib/src/__tests__/TimePicker.test.tsx similarity index 99% rename from lib/src/__tests__/e2e/TimePicker.test.tsx rename to lib/src/__tests__/TimePicker.test.tsx index 3967b574e..87ad68344 100644 --- a/lib/src/__tests__/e2e/TimePicker.test.tsx +++ b/lib/src/__tests__/TimePicker.test.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { ReactWrapper } from 'enzyme'; import { clickOKButton } from './commands'; -import { mount, utilsToUse, toHaveBeenCalledExceptMoment } from '../test-utils'; +import { mount, utilsToUse, toHaveBeenCalledExceptMoment } from './test-utils'; import { MobileTimePicker, DesktopTimePicker, TimePicker, TimePickerProps, -} from '../../TimePicker/TimePicker'; +} from '../TimePicker/TimePicker'; const fakeTouchEvent = { buttons: 1, diff --git a/lib/src/__tests__/e2e/commands.tsx b/lib/src/__tests__/commands.tsx similarity index 100% rename from lib/src/__tests__/e2e/commands.tsx rename to lib/src/__tests__/commands.tsx diff --git a/lib/src/__tests__/setup.js b/lib/src/__tests__/setup.js index 765002fde..d1754915c 100644 --- a/lib/src/__tests__/setup.js +++ b/lib/src/__tests__/setup.js @@ -4,6 +4,18 @@ const EnzymeAdapter = require('enzyme-adapter-react-16'); // Setup enzyme's react adapter Enzyme.configure({ adapter: new EnzymeAdapter() }); +// Fix popper mount https://github.com/mui-org/material-ui/issues/15726 +// TODO, upgrade jsdom to 16.0.0 and remove this line. +document.createRange = () => ({ + setStart: () => {}, + setEnd: () => {}, + // @ts-ignore + commonAncestorContainer: { + nodeName: 'BODY', + ownerDocument: document, + }, +}); + // Convert any console error into a thrown error const error = console.error; console.error = (...args) => { diff --git a/lib/src/__tests__/_shared/ModalDialog.test.tsx b/lib/src/__tests__/shallow/ModalDialog.test.tsx similarity index 100% rename from lib/src/__tests__/_shared/ModalDialog.test.tsx rename to lib/src/__tests__/shallow/ModalDialog.test.tsx diff --git a/lib/src/__tests__/DatePicker/Month.test.tsx b/lib/src/__tests__/shallow/Month.test.tsx similarity index 100% rename from lib/src/__tests__/DatePicker/Month.test.tsx rename to lib/src/__tests__/shallow/Month.test.tsx diff --git a/lib/src/__tests__/DatePicker/MonthSelection.test.tsx b/lib/src/__tests__/shallow/MonthSelection.test.tsx similarity index 100% rename from lib/src/__tests__/DatePicker/MonthSelection.test.tsx rename to lib/src/__tests__/shallow/MonthSelection.test.tsx diff --git a/lib/src/__tests__/test-utils.tsx b/lib/src/__tests__/test-utils.tsx index 1165c4653..3b8acd389 100644 --- a/lib/src/__tests__/test-utils.tsx +++ b/lib/src/__tests__/test-utils.tsx @@ -5,8 +5,8 @@ import MomentUtils from '@date-io/moment'; import DateFnsUtils from '@date-io/date-fns'; import LocalizationProvider from '../LocalizationProvider'; import { IUtils } from '@date-io/core/IUtils'; -import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import { MaterialUiPickersDate } from '../typings/date'; +import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; interface WithUtilsProps { utils: IUtils; diff --git a/lib/src/__tests__/tsconfig.json b/lib/src/__tests__/tsconfig.json index 26187c5b2..9283d45cc 100644 --- a/lib/src/__tests__/tsconfig.json +++ b/lib/src/__tests__/tsconfig.json @@ -1,5 +1,5 @@ { "extends": "../../tsconfig.json", - "include": ["./**/*.tsx", "../../typings.d.ts"], + "include": ["../../typings.d.ts", "./**/*.tsx", "./**/*.ts"], "exclude": [] } diff --git a/lib/src/__tests__/unit/date-range-manager.test.ts b/lib/src/__tests__/unit/date-range-manager.test.ts new file mode 100644 index 000000000..7d2397de9 --- /dev/null +++ b/lib/src/__tests__/unit/date-range-manager.test.ts @@ -0,0 +1,61 @@ +import { utilsToUse } from '../test-utils'; +import { + calculateRangeChange, + calculateRangePreview, +} from '../../DateRangePicker/date-range-manager'; + +const start2018 = utilsToUse.date(new Date('2018-01-01T00:00:00.000Z')); +const mid2018 = utilsToUse.date(new Date('2018-06-01T00:00:00.000Z')); +const end2019 = utilsToUse.date(new Date('2019-01-01T00:00:00.000Z')); + +test.each` + range | selectingEnd | newDate | expectedRange | expectedNextSelection + ${[null, null]} | ${'start'} | ${start2018} | ${[start2018, null]} | ${'end'} + ${[start2018, null]} | ${'start'} | ${end2019} | ${[end2019, null]} | ${'end'} + ${[null, end2019]} | ${'start'} | ${mid2018} | ${[mid2018, end2019]} | ${'end'} + ${[null, end2019]} | ${'end'} | ${mid2018} | ${[null, mid2018]} | ${'start'} + ${[mid2018, null]} | ${'start'} | ${start2018} | ${[start2018, null]} | ${'end'} + ${[start2018, end2019]} | ${'start'} | ${mid2018} | ${[mid2018, end2019]} | ${'end'} + ${[start2018, end2019]} | ${'end'} | ${mid2018} | ${[start2018, mid2018]} | ${'start'} + ${[mid2018, end2019]} | ${'start'} | ${start2018} | ${[start2018, end2019]} | ${'end'} + ${[start2018, mid2018]} | ${'end'} | ${mid2018} | ${[start2018, mid2018]} | ${'start'} +`( + 'calculateRangeChange should return $expectedRange when selecting $selectingEnd of $range with user input $newDate', + ({ range, selectingEnd, newDate, expectedRange, expectedNextSelection }) => { + expect( + calculateRangeChange({ + utils: utilsToUse, + range, + newDate, + currentlySelectingRangeEnd: selectingEnd, + }) + ).toEqual({ + nextSelection: expectedNextSelection, + newRange: expectedRange, + }); + } +); + +test.each` + range | selectingEnd | newDate | expectedRange + ${[start2018, end2019]} | ${'start'} | ${null} | ${[null, null]} + ${[null, null]} | ${'start'} | ${start2018} | ${[start2018, null]} + ${[start2018, null]} | ${'start'} | ${end2019} | ${[end2019, null]} + ${[null, end2019]} | ${'start'} | ${mid2018} | ${[mid2018, end2019]} + ${[null, end2019]} | ${'end'} | ${mid2018} | ${[null, mid2018]} + ${[mid2018, null]} | ${'start'} | ${start2018} | ${[start2018, null]} + ${[mid2018, end2019]} | ${'start'} | ${start2018} | ${[start2018, mid2018]} + ${[start2018, mid2018]} | ${'end'} | ${end2019} | ${[mid2018, end2019]} +`( + 'calculateRangePreview should return $expectedRange when selecting $selectingEnd of $range when user hover $newDate', + ({ range, selectingEnd, newDate, expectedRange }) => { + expect( + calculateRangePreview({ + utils: utilsToUse, + range, + newDate, + currentlySelectingRangeEnd: selectingEnd, + }) + ).toEqual(expectedRange); + } +); diff --git a/lib/src/__tests__/_helpers/date-utils.test.tsx b/lib/src/__tests__/unit/date-utils.test.ts similarity index 100% rename from lib/src/__tests__/_helpers/date-utils.test.tsx rename to lib/src/__tests__/unit/date-utils.test.ts diff --git a/lib/src/__tests__/_helpers/text-field-helper.test.tsx b/lib/src/__tests__/unit/text-field-helper.test.ts similarity index 100% rename from lib/src/__tests__/_helpers/text-field-helper.test.tsx rename to lib/src/__tests__/unit/text-field-helper.test.ts diff --git a/lib/src/_helpers/date-utils.ts b/lib/src/_helpers/date-utils.ts index 0ce13a08c..351a26bd7 100644 --- a/lib/src/_helpers/date-utils.ts +++ b/lib/src/_helpers/date-utils.ts @@ -3,6 +3,7 @@ import { IUtils } from '@date-io/core/IUtils'; import { MaterialUiPickersDate } from '../typings/date'; import { BasePickerProps } from '../typings/BasePicker'; import { DatePickerView } from '../DatePicker/DatePicker'; +import { DateRange } from '../DateRangePicker/RangeTypes'; import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; interface FindClosestDateParams { @@ -106,3 +107,34 @@ export function parsePickerInputValue( return parsedValue && utils.isValid(parsedValue) ? parsedValue : now; } + +export const isRangeValid = ( + utils: MuiPickersAdapter, + range: DateRange | null +): range is DateRange => { + return Boolean(range && range[0] && range[1] && utils.isBefore(range[0], range[1])); +}; + +export const isWithinRange = ( + utils: MuiPickersAdapter, + day: MaterialUiPickersDate, + range: DateRange | null +) => { + return isRangeValid(utils, range) && utils.isWithinRange(day, range); +}; + +export const isStartOfRange = ( + utils: MuiPickersAdapter, + day: MaterialUiPickersDate, + range: DateRange | null +) => { + return isRangeValid(utils, range) && utils.isSameDay(day, range[0]); +}; + +export const isEndOfRange = ( + utils: MuiPickersAdapter, + day: MaterialUiPickersDate, + range: DateRange | null +) => { + return isRangeValid(utils, range) && utils.isSameDay(day, range[1]); +}; diff --git a/lib/src/_helpers/utils.ts b/lib/src/_helpers/utils.ts index 656885111..80d370254 100644 --- a/lib/src/_helpers/utils.ts +++ b/lib/src/_helpers/utils.ts @@ -9,7 +9,10 @@ export function arrayIncludes(array: T[] | readonly T[], itemOrItems: T | T[] return array.indexOf(itemOrItems) !== -1; } -export const onSpaceOrEnter = (innerFn: () => void) => (e: React.KeyboardEvent) => { +export const onSpaceOrEnter = ( + innerFn: () => void, + onFocus?: (e: React.KeyboardEvent) => void +) => (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { innerFn(); @@ -17,6 +20,10 @@ export const onSpaceOrEnter = (innerFn: () => void) => (e: React.KeyboardEvent) e.preventDefault(); e.stopPropagation(); } + + if (onFocus) { + onFocus(e); + } }; /** Quick untyped helper to improve function composition readability */ @@ -25,3 +32,33 @@ export const pipe = (...fns: ((...args: any[]) => any)[]) => (prevFn, nextFn) => (...args) => nextFn(prevFn(...args)), value => value ); + +export const executeInTheNextEventLoopTick = (fn: () => void) => setTimeout(fn, 0); + +export function createDelegatedEventHandler( + fn: (e: TEvent) => void, + onEvent?: (e: TEvent) => void +) { + return (e: TEvent) => { + fn(e); + + if (onEvent) { + onEvent(e); + } + }; +} + +export function mergeRefs(refs: (React.Ref | undefined)[]) { + return (value: T) => { + refs.forEach(ref => { + if (typeof ref === 'function') { + ref(value); + } else if (typeof ref === 'object' && ref != null) { + // @ts-ignore .current is not a readonly, hold on ts + ref.current = value; + } + }); + }; +} + +export const doNothing = () => {}; diff --git a/lib/src/_shared/ArrowSwitcher.tsx b/lib/src/_shared/ArrowSwitcher.tsx index 81c81f60b..3ea35d481 100644 --- a/lib/src/_shared/ArrowSwitcher.tsx +++ b/lib/src/_shared/ArrowSwitcher.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import clsx from 'clsx'; +import Typography from '@material-ui/core/Typography'; +import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; import { ArrowLeftIcon } from './icons/ArrowLeftIcon'; import { ArrowRightIcon } from './icons/ArrowRightIcon'; -import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; import { makeStyles, useTheme } from '@material-ui/core/styles'; export interface ExportedArrowSwitcherProps { @@ -27,34 +28,46 @@ export interface ExportedArrowSwitcherProps { } interface ArrowSwitcherProps extends ExportedArrowSwitcherProps, React.HTMLProps { + isLeftHidden?: boolean; + isRightHidden?: boolean; isLeftDisabled: boolean; isRightDisabled: boolean; onLeftClick: () => void; onRightClick: () => void; + text?: string; } -const useStyles = makeStyles(theme => ({ - iconButton: { - zIndex: 1, - backgroundColor: theme.palette.background.paper, - }, - previousMonthButton: { - marginRight: 24, - }, -})); +export const useStyles = makeStyles( + theme => ({ + iconButton: { + zIndex: 1, + backgroundColor: theme.palette.background.paper, + }, + previousMonthButtonMargin: { + marginRight: 24, + }, + hidden: { + visibility: 'hidden', + }, + }), + { name: 'MuiPickersArrowSwitcher' } +); -export const ArrowSwitcher: React.FC = ({ +const PureArrowSwitcher: React.FC = ({ className, leftArrowButtonProps, leftArrowButtonText, rightArrowButtonProps, rightArrowButtonText, + isLeftHidden, + isRightHidden, isLeftDisabled, isRightDisabled, onLeftClick, onRightClick, leftArrowIcon = , rightArrowIcon = , + text, ...other }) => { const classes = useStyles(); @@ -70,15 +83,20 @@ export const ArrowSwitcher: React.FC = ({ {...leftArrowButtonProps} disabled={isLeftDisabled} onClick={onLeftClick} - className={clsx( - classes.iconButton, - classes.previousMonthButton, - leftArrowButtonProps?.className - )} + className={clsx(classes.iconButton, leftArrowButtonProps?.className, { + [classes.hidden]: Boolean(isLeftHidden), + [classes.previousMonthButtonMargin]: !Boolean(text), + })} > {isRtl ? rightArrowIcon : leftArrowIcon} + {text && ( + + {text} + + )} + = ({ {...rightArrowButtonProps} disabled={isRightDisabled} onClick={onRightClick} - className={clsx(classes.iconButton, rightArrowButtonProps?.className)} + className={clsx(classes.iconButton, rightArrowButtonProps?.className, { + [classes.hidden]: Boolean(isRightHidden), + })} > {isRtl ? leftArrowIcon : rightArrowIcon}
); }; + +PureArrowSwitcher.displayName = 'ArrowSwitcher'; + +export const ArrowSwitcher = React.memo(PureArrowSwitcher); diff --git a/lib/src/_shared/KeyboardDateInput.tsx b/lib/src/_shared/KeyboardDateInput.tsx index 78a606269..3101a1689 100644 --- a/lib/src/_shared/KeyboardDateInput.tsx +++ b/lib/src/_shared/KeyboardDateInput.tsx @@ -4,8 +4,9 @@ import IconButton from '@material-ui/core/IconButton'; import InputAdornment from '@material-ui/core/InputAdornment'; import { Rifm } from 'rifm'; import { useUtils } from './hooks/useUtils'; -import { DateInputProps } from './PureDateInput'; import { KeyboardIcon } from './icons/KeyboardIcon'; +import { DateInputProps, DateInputRefs } from './PureDateInput'; +import { createDelegatedEventHandler } from '../_helpers/utils'; import { maskedDateFormatter, getDisplayDate, @@ -14,7 +15,7 @@ import { staticDateWith2DigitTokens, } from '../_helpers/text-field-helper'; -export const KeyboardDateInput: React.FC = ({ +export const KeyboardDateInput: React.FC = ({ disableMaskedInput, rawValue, validationError, @@ -33,17 +34,22 @@ export const KeyboardDateInput: React.FC = ({ keyboardIcon = , variant, emptyInputText: emptyLabel, - hideOpenPickerButton, + disableOpenPicker: hideOpenPickerButton, ignoreInvalidInputs, onFocus, onBlur, + parsedDateValue, forwardedRef, containerRef, + open, + readOnly, + inputProps: inputPropsPassed, getOpenDialogAriaText = getTextFieldAriaText, ...other }) => { const utils = useUtils(); const [isFocused, setIsFocused] = React.useState(false); + const getInputValue = () => getDisplayDate(rawValue, utils, { inputFormat, @@ -103,6 +109,10 @@ export const KeyboardDateInput: React.FC = ({ helperText: validationError, 'data-mui-test': 'keyboard-date-input', ...other, + inputProps: { + ...inputPropsPassed, + readOnly, + }, InputProps: { ...InputProps, [`${adornmentPosition}Adornment`]: hideOpenPickerButton ? ( @@ -130,14 +140,8 @@ export const KeyboardDateInput: React.FC = ({ value={innerInputValue || ''} onChange={e => handleChange(e.currentTarget.value)} {...inputProps} - onFocus={e => { - setIsFocused(true); - onFocus && onFocus(e); - }} - onBlur={e => { - setIsFocused(false); - onBlur && onBlur(e); - }} + onFocus={createDelegatedEventHandler(() => setIsFocused(true), onFocus)} + onBlur={createDelegatedEventHandler(() => setIsFocused(true), onBlur)} /> ); } diff --git a/lib/src/_shared/PickerToolbar.tsx b/lib/src/_shared/PickerToolbar.tsx index 33a8cda02..9c7683770 100644 --- a/lib/src/_shared/PickerToolbar.tsx +++ b/lib/src/_shared/PickerToolbar.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import clsx from 'clsx'; -import Toolbar, { ToolbarProps } from '@material-ui/core/Toolbar'; -import { makeStyles } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; -import Grid from '@material-ui/core/Grid'; +import Toolbar, { ToolbarProps } from '@material-ui/core/Toolbar'; import { ExtendMui } from '../typings/helpers'; import { PenIcon } from '../_shared/icons/PenIcon'; import { KeyboardIcon } from './icons/KeyboardIcon'; +import { makeStyles } from '@material-ui/core/styles'; import { ToolbarComponentProps } from '../Picker/Picker'; export const useStyles = makeStyles( diff --git a/lib/src/_shared/PureDateInput.tsx b/lib/src/_shared/PureDateInput.tsx index 16aa3d6d7..5639fbcb5 100644 --- a/lib/src/_shared/PureDateInput.tsx +++ b/lib/src/_shared/PureDateInput.tsx @@ -1,20 +1,22 @@ import * as React from 'react'; import TextField, { TextFieldProps } from '@material-ui/core/TextField'; -import { IconButtonProps } from '@material-ui/core/IconButton'; -import { InputAdornmentProps } from '@material-ui/core/InputAdornment'; import { ExtendMui } from '../typings/helpers'; import { onSpaceOrEnter } from '../_helpers/utils'; import { ParsableDate } from '../constants/prop-types'; import { MaterialUiPickersDate } from '../typings/date'; +import { IconButtonProps } from '@material-ui/core/IconButton'; import { useUtils, MuiPickersAdapter } from './hooks/useUtils'; +import { InputAdornmentProps } from '@material-ui/core/InputAdornment'; import { getDisplayDate, getTextFieldAriaText } from '../_helpers/text-field-helper'; export interface DateInputProps extends ExtendMui { rawValue: TInputValue; + parsedDateValue: TDateValue; inputFormat: string; - onChange: (date: TDateValue | null, keyboardInputValue?: string) => void; + onChange: (date: TDateValue, keyboardInputValue?: string) => void; openPicker: () => void; + readOnly?: boolean; validationError?: React.ReactNode; /** Override input component */ TextFieldComponent?: React.ComponentType; @@ -55,7 +57,7 @@ export interface DateInputProps string; // ?? TODO when it will be possible to display "empty" date in datepicker use it instead of ignoring invalid inputs ignoreInvalidInputs?: boolean; - containerRef?: React.Ref; - forwardedRef?: React.Ref; + open: boolean; } -export type ExportedDateInputProps = Omit< - DateInputProps, +export type ExportedDateInputProps = Omit< + DateInputProps, | 'openPicker' | 'inputValue' | 'onChange' @@ -78,9 +79,16 @@ export type ExportedDateInputProps = Omit< | 'validationError' | 'rawValue' | 'forwardedRef' + | 'parsedDateValue' + | 'open' >; -export const PureDateInput: React.FC = ({ +export interface DateInputRefs { + containerRef?: React.Ref; + forwardedRef?: React.Ref; +} + +export const PureDateInput: React.FC = ({ onChange, inputFormat, rifmFormatter, @@ -95,12 +103,14 @@ export const PureDateInput: React.FC = ({ variant, emptyInputText: emptyLabel, keyboardIcon, - hideOpenPickerButton, + disableOpenPicker: hideOpenPickerButton, ignoreInvalidInputs, KeyboardButtonProps, disableMaskedInput, + parsedDateValue, forwardedRef, containerRef, + open, getOpenDialogAriaText = getTextFieldAriaText, ...other }) => { diff --git a/lib/src/_shared/ToolbarButton.tsx b/lib/src/_shared/ToolbarButton.tsx index 179a267a2..a5117e966 100644 --- a/lib/src/_shared/ToolbarButton.tsx +++ b/lib/src/_shared/ToolbarButton.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import clsx from 'clsx'; +import ToolbarText from './ToolbarText'; import Button, { ButtonProps } from '@material-ui/core/Button'; +import { ExtendMui } from '../typings/helpers'; import { makeStyles } from '@material-ui/core/styles'; import { TypographyProps } from '@material-ui/core/Typography'; -import ToolbarText from './ToolbarText'; -import { ExtendMui } from '../typings/helpers'; -export interface ToolbarButtonProps extends ExtendMui { +export interface ToolbarButtonProps extends ExtendMui { variant: TypographyProps['variant']; selected: boolean; - label: string; + value: React.ReactNode; align?: TypographyProps['align']; typographyClassName?: string; } @@ -25,9 +25,9 @@ export const useStyles = makeStyles( { name: 'MuiPickersToolbarButton' } ); -const ToolbarButton: React.FunctionComponent = ({ +export const ToolbarButton: React.FunctionComponent = ({ className = null, - label, + value: label, selected, variant, align, @@ -42,11 +42,13 @@ const ToolbarButton: React.FunctionComponent = ({ align={align} className={typographyClassName} variant={variant} - label={label} + value={label} selected={selected} /> ); }; +ToolbarButton.displayName = 'ToolbarButton'; + export default ToolbarButton; diff --git a/lib/src/_shared/ToolbarText.tsx b/lib/src/_shared/ToolbarText.tsx index 51407e5c6..36151b819 100644 --- a/lib/src/_shared/ToolbarText.tsx +++ b/lib/src/_shared/ToolbarText.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import clsx from 'clsx'; import Typography, { TypographyProps } from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; -import { fade } from '@material-ui/core/styles/colorManipulator'; import { ExtendMui } from '../typings/helpers'; +import { makeStyles, fade } from '@material-ui/core/styles'; + export interface ToolbarTextProps extends ExtendMui { selected?: boolean; - label: string; + value: React.ReactNode; } export const useStyles = makeStyles( @@ -18,6 +18,7 @@ export const useStyles = makeStyles( return { toolbarTxt: { + transition: theme.transitions.create('color'), color: fade(textColor, 0.54), }, toolbarBtnSelected: { @@ -28,9 +29,9 @@ export const useStyles = makeStyles( { name: 'MuiPickersToolbarText' } ); -const ToolbarText: React.FunctionComponent = ({ +const ToolbarText: React.FC = ({ selected, - label, + value: label, className = null, ...other }) => { diff --git a/lib/src/_shared/hooks/date-helpers-hooks.tsx b/lib/src/_shared/hooks/date-helpers-hooks.tsx new file mode 100644 index 000000000..95c077e1e --- /dev/null +++ b/lib/src/_shared/hooks/date-helpers-hooks.tsx @@ -0,0 +1,48 @@ +import * as React from 'react'; +import { useUtils } from './useUtils'; +import { MaterialUiPickersDate } from '../../typings/date'; + +export function useParsedDate(possiblyUnparsedValue: any) { + const utils = useUtils(); + return React.useMemo( + () => + typeof possiblyUnparsedValue === 'undefined' ? undefined : utils.date(possiblyUnparsedValue)!, + [possiblyUnparsedValue, utils] + ); +} + +interface MonthValidationOptions { + disablePast?: boolean; + disableFuture?: boolean; + minDate: MaterialUiPickersDate; + maxDate: MaterialUiPickersDate; +} + +export function useNextMonthDisabled( + month: MaterialUiPickersDate, + { disableFuture, maxDate }: Pick +) { + const utils = useUtils(); + return React.useMemo(() => { + const now = utils.date(); + const lastEnabledMonth = utils.startOfMonth( + disableFuture && utils.isBefore(now, maxDate) ? now : maxDate + ); + return !utils.isAfter(lastEnabledMonth, month); + }, [disableFuture, maxDate, month, utils]); +} + +export function usePreviousMonthDisabled( + month: MaterialUiPickersDate, + { disablePast, minDate }: Pick +) { + const utils = useUtils(); + + return React.useMemo(() => { + const now = utils.date(); + const firstEnabledMonth = utils.startOfMonth( + disablePast && utils.isAfter(now, minDate) ? now : minDate + ); + return !utils.isBefore(firstEnabledMonth, month); + }, [disablePast, minDate, month, utils]); +} diff --git a/lib/src/_shared/hooks/useKeyDown.ts b/lib/src/_shared/hooks/useKeyDown.ts index 65ffd7be0..cab390f8d 100644 --- a/lib/src/_shared/hooks/useKeyDown.ts +++ b/lib/src/_shared/hooks/useKeyDown.ts @@ -55,4 +55,5 @@ export const keycode = { End: 35, PageUp: 33, PageDown: 34, + Esc: 27, }; diff --git a/lib/src/_shared/hooks/useOpenState.ts b/lib/src/_shared/hooks/useOpenState.ts index c6fbc5e10..c3dc63b21 100644 --- a/lib/src/_shared/hooks/useOpenState.ts +++ b/lib/src/_shared/hooks/useOpenState.ts @@ -1,22 +1,32 @@ -/* eslint-disable react-hooks/rules-of-hooks */ +import * as React from 'react'; import { BasePickerProps } from '../../typings/BasePicker'; -import { useCallback, useState, Dispatch, SetStateAction } from 'react'; export function useOpenState({ open, onOpen, onClose }: BasePickerProps) { - let setIsOpenState: null | Dispatch> = null; - if (open === undefined || open === null) { - // The component is uncontrolled, so we need to give it its own state. - [open, setIsOpenState] = useState(false); - } + const isControllingOpenProp = React.useRef(typeof open === 'boolean').current; + const [_open, _setIsOpen] = React.useState(false); - // prettier-ignore - const setIsOpen = useCallback((newIsOpen: boolean) => { - setIsOpenState && setIsOpenState(newIsOpen); + // It is required to update inner state in useEffect in order to avoid situation when + // Our component is not mounted yet, but `open` state is set to `true` (e.g. initially opened) + React.useEffect(() => { + if (isControllingOpenProp) { + if (typeof open !== 'boolean') { + throw new Error('You must not mix controlling and uncontrolled mode for `open` prop'); + } - return newIsOpen - ? onOpen && onOpen() - : onClose && onClose(); - }, [onOpen, onClose, setIsOpenState]); + _setIsOpen(open); + } + }, [isControllingOpenProp, open]); - return { isOpen: open, setIsOpen }; + const setIsOpen = React.useCallback( + (newIsOpen: boolean) => { + if (!isControllingOpenProp) { + _setIsOpen(newIsOpen); + } + + return newIsOpen ? onOpen && onOpen() : onClose && onClose(); + }, + [isControllingOpenProp, onOpen, onClose] + ); + + return { isOpen: _open, setIsOpen }; } diff --git a/lib/src/_shared/hooks/useParsedDate.tsx b/lib/src/_shared/hooks/useParsedDate.tsx deleted file mode 100644 index a21c97e2e..000000000 --- a/lib/src/_shared/hooks/useParsedDate.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import { useUtils } from './useUtils'; - -export function useParsedDate(possiblyUnparsedValue: any) { - const utils = useUtils(); - return React.useMemo( - () => - typeof possiblyUnparsedValue === 'undefined' ? undefined : utils.date(possiblyUnparsedValue)!, - [possiblyUnparsedValue, utils] - ); -} diff --git a/lib/src/_shared/hooks/usePickerState.ts b/lib/src/_shared/hooks/usePickerState.ts index 49976b2c0..62316b9e3 100644 --- a/lib/src/_shared/hooks/usePickerState.ts +++ b/lib/src/_shared/hooks/usePickerState.ts @@ -7,18 +7,22 @@ import { useCallback, useDebugValue, useEffect, useMemo, useState } from 'react' export const FORCE_FINISH_PICKER = Symbol('Force closing picker, used for accessibility '); -export function usePickerState( - props: BasePickerProps, - parseInputValue: ( - now: MaterialUiPickersDate, - utils: MuiPickersAdapter, - props: BasePickerProps - ) => TOutput | null, - validateInputValue: ( - value: TInput, - utils: MuiPickersAdapter, - props: BasePickerProps - ) => React.ReactNode | undefined +export function usePickerState( + props: BasePickerProps, + valueManager: { + parseInput: ( + now: MaterialUiPickersDate, + utils: MuiPickersAdapter, + props: BasePickerProps + ) => TDateValue; + validateInput: ( + value: TInput, + utils: MuiPickersAdapter, + props: BasePickerProps + ) => React.ReactNode | undefined; + emptyValue: TDateValue; + areValuesEqual: (valueLeft: TDateValue, valueRight: TDateValue) => boolean; + } ) { const { autoOk, inputFormat, disabled, readOnly, onAccept, onChange, onError, value } = props; @@ -28,7 +32,7 @@ export function usePickerState( const now = useNow(); const utils = useUtils(); - const date = parseInputValue(now, utils, props); + const date = valueManager.parseInput(now, utils, props); const [pickerDate, setPickerDate] = useState(date); // Mobile keyboard view is a special case. @@ -37,14 +41,13 @@ export function usePickerState( const { isOpen, setIsOpen } = useOpenState(props); useEffect(() => { - // if value was changed in closed state or from mobile keyboard view - treat it as accepted - if ((!isOpen || isMobileKeyboardViewOpen) && !utils.isEqual(pickerDate, date)) { + if (!valueManager.areValuesEqual(pickerDate, date)) { setPickerDate(date); } - }, [date, isMobileKeyboardViewOpen, isOpen, pickerDate, utils]); + }, [value]); // eslint-disable-line const acceptDate = useCallback( - (acceptedDate: TOutput | null, needClosePicker: boolean) => { + (acceptedDate: TDateValue, needClosePicker: boolean) => { onChange(acceptedDate); if (needClosePicker) { @@ -61,8 +64,7 @@ export function usePickerState( const wrapperProps = useMemo( () => ({ open: isOpen, - format: inputFormat, - onClear: () => acceptDate(null, true), + onClear: () => acceptDate(valueManager.emptyValue, true), onAccept: () => acceptDate(pickerDate, true), onDismiss: () => setIsOpen(false), onSetToday: () => { @@ -71,7 +73,7 @@ export function usePickerState( acceptDate(now as any, Boolean(autoOk)); }, }), - [acceptDate, autoOk, inputFormat, isOpen, now, pickerDate, setIsOpen] + [acceptDate, autoOk, isOpen, now, pickerDate, setIsOpen, valueManager.emptyValue] ); const pickerProps = useMemo( @@ -87,7 +89,7 @@ export function usePickerState( setMobileKeyboardViewOpen(!isMobileKeyboardViewOpen); }, onDateChange: ( - newDate: TOutput, + newDate: TDateValue, currentVariant: WrapperVariant, isFinish: boolean | symbol = true ) => { @@ -110,7 +112,7 @@ export function usePickerState( [acceptDate, autoOk, isMobileKeyboardViewOpen, pickerDate] ); - const validationError = validateInputValue(value, utils, props); + const validationError = valueManager.validateInput(value, utils, props); useEffect(() => { if (onError) { onError(validationError, value); @@ -121,11 +123,23 @@ export function usePickerState( () => ({ onChange, inputFormat, + open: isOpen, rawValue: value, validationError, + parsedDateValue: pickerDate, openPicker: () => !readOnly && !disabled && setIsOpen(true), }), - [disabled, inputFormat, onChange, readOnly, setIsOpen, validationError, value] + [ + onChange, + inputFormat, + isOpen, + value, + validationError, + pickerDate, + readOnly, + disabled, + setIsOpen, + ] ); const pickerState = { pickerProps, inputProps, wrapperProps }; diff --git a/lib/src/_shared/hooks/useTraceUpdate.tsx b/lib/src/_shared/hooks/useTraceUpdate.tsx new file mode 100644 index 000000000..cd5ac981b --- /dev/null +++ b/lib/src/_shared/hooks/useTraceUpdate.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +// ! Important not to use this hook in production build + +export function useDebuggingTraceUpdate(props: any) { + const prev = React.useRef(props); + React.useEffect(() => { + // @ts-ignore + const changedProps = Object.entries(props).reduce((ps, [k, v]) => { + if (prev.current[k] !== v) { + ps[k] = [prev.current[k], v]; + } + return ps; + }, {}); + if (Object.keys(changedProps).length > 0) { + console.log('Changed props:', changedProps); + } + prev.current = props; + }); +} diff --git a/lib/src/_shared/withDateAdapterProp.tsx b/lib/src/_shared/withDateAdapterProp.tsx index 6c5e5b71d..9911a238c 100644 --- a/lib/src/_shared/withDateAdapterProp.tsx +++ b/lib/src/_shared/withDateAdapterProp.tsx @@ -4,7 +4,7 @@ import { MuiPickersAdapterContext } from '../LocalizationProvider'; export interface WithDateAdapterProps { /** - * Allows to pass configured date-io adapter directly. More info [here](/guides/date-adapter-passing) + * Allows to pass configured date-io adapter directly. More info [here](https://material-ui-pickers.dev/guides/date-adapter-passing) * ```jsx * dateAdapter={new DateFnsAdapter({ locale: ruLocale })} * ``` @@ -19,7 +19,7 @@ export function withDateAdapterProp( if (dateAdapter) { return ( - + ); } diff --git a/lib/src/constants/dimensions.ts b/lib/src/constants/dimensions.ts index 5fc349d12..1df551de7 100644 --- a/lib/src/constants/dimensions.ts +++ b/lib/src/constants/dimensions.ts @@ -2,4 +2,8 @@ export const DIALOG_WIDTH = 320; export const DIALOG_WIDTH_WIDER = 325; -export const VIEW_HEIGHT = 330; +export const VIEW_HEIGHT = 358; + +export const DAY_SIZE = 36; + +export const DAY_MARGIN = 2; diff --git a/lib/src/index.ts b/lib/src/index.ts index 53d9eceb3..890085817 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -6,6 +6,8 @@ export * from './TimePicker'; export * from './DateTimePicker'; +export * from './DateRangePicker/DateRangePicker'; + export { Calendar } from './views/Calendar/Calendar'; export * from './views/Calendar/CalendarView'; diff --git a/lib/src/typings/BasePicker.tsx b/lib/src/typings/BasePicker.tsx index 853437d60..a91c9957c 100644 --- a/lib/src/typings/BasePicker.tsx +++ b/lib/src/typings/BasePicker.tsx @@ -9,7 +9,7 @@ export interface BasePickerProps< /** Picker value */ value: TInputValue; /** onChange callback @DateIOType */ - onChange: (date: TDateValue | null, keyboardInputValue?: string) => void; + onChange: (date: TDateValue, keyboardInputValue?: string) => void; /** * Auto accept date on selection * @default false @@ -24,7 +24,7 @@ export interface BasePickerProps< /** Date that will be initially highlighted if null was passed */ defaultHighlight?: ParsableDate; /** Callback fired when date is accepted @DateIOType */ - onAccept?: (date: TDateValue | null) => void; + onAccept?: (date: TDateValue) => void; /** Callback fired when new error should be displayed * (!! This is a side effect. Be careful if you want to rerender the component) @DateIOType */ diff --git a/lib/src/typings/overrides.ts b/lib/src/typings/overrides.ts index 37e20d45c..9fc1b83c1 100644 --- a/lib/src/typings/overrides.ts +++ b/lib/src/typings/overrides.ts @@ -61,4 +61,12 @@ export interface MuiPickersOverrides { MuiPickersDatePickerRoot?: Classes; MuiPickerDTToolbar?: Classes; MuiBasePickerStyles?: Classes; + // consider using inline import type notation + MuiPickersDesktopDateRangeCalendar?: Classes< + typeof import('../DateRangePicker/DateRangePickerViewDesktop').useStyles + >; + MuiPickersArrowSwitcher?: Classes; + MuiPickersDateRangePickerInput?: Classes< + typeof import('../DateRangePicker/DateRangePickerInput').useStyles + >; } diff --git a/lib/src/views/Calendar/Calendar.tsx b/lib/src/views/Calendar/Calendar.tsx index 99a9354af..41b0b7b59 100644 --- a/lib/src/views/Calendar/Calendar.tsx +++ b/lib/src/views/Calendar/Calendar.tsx @@ -1,19 +1,18 @@ import * as React from 'react'; +import clsx from 'clsx'; import Typography from '@material-ui/core/Typography'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import DayWrapper from './DayWrapper'; -import SlideTransition, { SlideDirection } from './SlideTransition'; import { Day, DayProps } from './Day'; import { MaterialUiPickersDate } from '../../typings/date'; import { useUtils, useNow } from '../../_shared/hooks/useUtils'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; +import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; import { findClosestEnabledDate } from '../../_helpers/date-utils'; import { useGlobalKeyDown, keycode } from '../../_shared/hooks/useKeyDown'; +import { SlideTransition, SlideDirection, SlideTransitionProps } from './SlideTransition'; export interface ExportedCalendarProps extends Pick { - /** Calendar Date @DateIOType */ - date: MaterialUiPickersDate; /** Calendar onChange */ onChange: PickerOnChangeFn; /** @@ -29,7 +28,7 @@ export interface ExportedCalendarProps /** Custom renderer for day. Check [DayComponentProps api](https://material-ui-pickers.dev/api/Day) @DateIOType */ renderDay?: ( day: MaterialUiPickersDate, - selectedDate: MaterialUiPickersDate, + selectedDates: MaterialUiPickersDate[], DayComponentProps: DayProps ) => JSX.Element; /** @@ -42,8 +41,9 @@ export interface ExportedCalendarProps } export interface CalendarProps extends ExportedCalendarProps { - minDate?: MaterialUiPickersDate; - maxDate?: MaterialUiPickersDate; + date: MaterialUiPickersDate | MaterialUiPickersDate[]; + minDate: MaterialUiPickersDate; + maxDate: MaterialUiPickersDate; isDateDisabled: (day: MaterialUiPickersDate) => boolean; slideDirection: SlideDirection; currentMonth: MaterialUiPickersDate; @@ -52,11 +52,16 @@ export interface CalendarProps extends ExportedCalendarProps { changeFocusedDay: (newFocusedDay: MaterialUiPickersDate) => void; isMonthSwitchingAnimating: boolean; onMonthSwitchingAnimationEnd: () => void; + className?: string; + TransitionProps?: Partial; } export const useStyles = makeStyles(theme => ({ transitionContainer: { - minHeight: 36 * 6 + 20, + minHeight: (DAY_SIZE + DAY_MARGIN * 4) * 6, + }, + transitionContainerOverflowAllowed: { + overflowX: 'visible', }, progressContainer: { width: '100%', @@ -66,6 +71,7 @@ export const useStyles = makeStyles(theme => ({ alignItems: 'center', }, week: { + margin: `${DAY_MARGIN}px 0`, display: 'flex', justifyContent: 'center', }, @@ -112,6 +118,8 @@ export const Calendar: React.FC = ({ isDateDisabled, disableHighlightToday, showDaysOutsideCurrentMonth, + className, + TransitionProps, }) => { const now = useNow(); const utils = useUtils(); @@ -120,16 +128,17 @@ export const Calendar: React.FC = ({ const handleDaySelect = React.useCallback( (day: MaterialUiPickersDate, isFinish: boolean | symbol = true) => { - onChange(utils.mergeDateAndTime(day, date), isFinish); + onChange(Array.isArray(date) ? day : utils.mergeDateAndTime(day, date), isFinish); }, [date, onChange, utils] ); + const initialDate = Array.isArray(date) ? date[0] : date; React.useEffect(() => { - if (isDateDisabled(date)) { + if (initialDate && isDateDisabled(initialDate)) { const closestEnabledDate = findClosestEnabledDate({ - date, utils, + date: initialDate, minDate: utils.date(minDate), maxDate: utils.date(maxDate), disablePast: Boolean(disablePast), @@ -141,7 +150,7 @@ export const Calendar: React.FC = ({ } }, []); // eslint-disable-line - const nowFocusedDay = focusedDay || date; + const nowFocusedDay = focusedDay || initialDate; useGlobalKeyDown(Boolean(allowKeyboardControl), { [keycode.ArrowUp]: () => changeFocusedDay(utils.addDays(nowFocusedDay, -7)), [keycode.ArrowDown]: () => changeFocusedDay(utils.addDays(nowFocusedDay, 7)), @@ -155,8 +164,10 @@ export const Calendar: React.FC = ({ [keycode.PageDown]: () => changeFocusedDay(utils.getPreviousMonth(nowFocusedDay)), }); - const selectedDate = utils.startOfDay(date); const currentMonthNumber = utils.getMonth(currentMonth); + const selectedDates = (Array.isArray(date) ? date : [date]) + .filter(Boolean) + .map(selectedDateItem => utils.startOfDay(selectedDateItem)); return ( <> @@ -173,53 +184,43 @@ export const Calendar: React.FC = ({
{utils.getWeekArray(currentMonth).map(week => ( -
+
{week.map(day => { const disabled = isDateDisabled(day); const isDayInCurrentMonth = utils.getMonth(day) === currentMonthNumber; - const dayProps = { + const dayProps: DayProps = { + key: (day as any)?.toString(), day: day, + role: 'cell', isAnimating: isMonthSwitchingAnimating, disabled: disabled, allowKeyboardControl: allowKeyboardControl, - focused: Boolean(focusedDay) && utils.isSameDay(day, focusedDay), - onFocus: () => changeFocusedDay(day), - isToday: utils.isSameDay(day, now), - hidden: !isDayInCurrentMonth, - isInCurrentMonth: isDayInCurrentMonth, - selected: utils.isSameDay(selectedDate, day), + focused: + allowKeyboardControl && Boolean(focusedDay) && utils.isSameDay(day, focusedDay), + today: utils.isSameDay(day, now), + inCurrentMonth: isDayInCurrentMonth, + selected: selectedDates.some(selectedDate => utils.isSameDay(selectedDate, day)), disableHighlightToday, showDaysOutsideCurrentMonth, focusable: + allowKeyboardControl && Boolean(nowFocusedDay) && utils.toJsDate(nowFocusedDay).getDate() === utils.toJsDate(day).getDate(), + onDayFocus: changeFocusedDay, + onDaySelect: handleDaySelect, }; - let dayComponent = renderDay ? ( - renderDay(day, selectedDate, dayProps) - ) : ( - - ); - - return ( - - ); + return renderDay ? renderDay(day, selectedDates, dayProps) : ; })}
))} @@ -229,4 +230,6 @@ export const Calendar: React.FC = ({ ); }; +Calendar.displayName = 'Calendar'; + export default Calendar; diff --git a/lib/src/views/Calendar/CalendarHeader.tsx b/lib/src/views/Calendar/CalendarHeader.tsx index 7594be468..ab9c1302a 100644 --- a/lib/src/views/Calendar/CalendarHeader.tsx +++ b/lib/src/views/Calendar/CalendarHeader.tsx @@ -1,26 +1,29 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import clsx from 'clsx'; +import Fade from '@material-ui/core/Fade'; import Typography from '@material-ui/core/Typography'; import IconButton from '@material-ui/core/IconButton'; -import Fade from '@material-ui/core/Fade'; -import { makeStyles } from '@material-ui/core/styles'; import { CalendarProps } from './Calendar'; import { DatePickerView } from '../../DatePicker'; import { SlideDirection } from './SlideTransition'; +import { makeStyles } from '@material-ui/core/styles'; import { useUtils } from '../../_shared/hooks/useUtils'; import { MaterialUiPickersDate } from '../../typings/date'; import { FadeTransitionGroup } from './FadeTransitionGroup'; import { ArrowDropDownIcon } from '../../_shared/icons/ArrowDropDownIcon'; import { ArrowSwitcher, ExportedArrowSwitcherProps } from '../../_shared/ArrowSwitcher'; +import { + usePreviousMonthDisabled, + useNextMonthDisabled, +} from '../../_shared/hooks/date-helpers-hooks'; export interface CalendarHeaderProps extends ExportedArrowSwitcherProps, Pick { view: DatePickerView; views: DatePickerView[]; - month: MaterialUiPickersDate; - + currentMonth: MaterialUiPickersDate; /** Get aria-label text for switching between views button */ getViewSwitchingButtonText?: (currentView: DatePickerView) => string; reduceAnimations: boolean; @@ -63,6 +66,7 @@ export const useStyles = makeStyles( display: 'flex', maxHeight: 30, overflow: 'hidden', + cursor: 'pointer', }, monthText: { marginRight: 4, @@ -80,7 +84,7 @@ function getSwitchingViewAriaText(view: DatePickerView) { export const CalendarHeader: React.SFC = ({ view, views, - month, + currentMonth: month, changeView, minDate, maxDate, @@ -102,23 +106,8 @@ export const CalendarHeader: React.SFC = ({ const selectNextMonth = () => onMonthChange(utils.getNextMonth(month), 'left'); const selectPreviousMonth = () => onMonthChange(utils.getPreviousMonth(month), 'right'); - const isPreviousMonthDisabled = React.useMemo(() => { - const now = utils.date(); - const firstEnabledMonth = utils.startOfMonth( - disablePast && utils.isAfter(now, minDate) ? now : minDate - ); - - return !utils.isBefore(firstEnabledMonth, month); - }, [disablePast, minDate, month, utils]); - - const isNextMonthDisabled = React.useMemo(() => { - const now = utils.date(); - const lastEnabledMonth = utils.startOfMonth( - disableFuture && utils.isBefore(now, maxDate) ? now : maxDate - ); - - return !utils.isAfter(lastEnabledMonth, month); - }, [disableFuture, maxDate, month, utils]); + const isNextMonthDisabled = useNextMonthDisabled(month, { disableFuture, maxDate }); + const isPreviousMonthDisabled = usePreviousMonthDisabled(month, { disablePast, minDate }); const toggleView = () => { if (views.length === 1) { @@ -134,14 +123,10 @@ export const CalendarHeader: React.SFC = ({ } }; - if (views.length === 1) { - return null; - } - return ( <>
-
+
= ({ /> - - - + {views.length > 1 && ( + + + + )}
diff --git a/lib/src/views/Calendar/CalendarView.tsx b/lib/src/views/Calendar/CalendarView.tsx index 0681cd8b0..973718070 100644 --- a/lib/src/views/Calendar/CalendarView.tsx +++ b/lib/src/views/Calendar/CalendarView.tsx @@ -1,20 +1,18 @@ import * as React from 'react'; -import { IUtils } from '@date-io/core/IUtils'; -import CircularProgress from '@material-ui/core/CircularProgress'; import Grid from '@material-ui/core/Grid'; -import { makeStyles } from '@material-ui/core/styles'; +import CircularProgress from '@material-ui/core/CircularProgress'; import { YearSelection } from './YearSelection'; import { MonthSelection } from './MonthSelection'; import { DatePickerView } from '../../DatePicker'; -import { SlideDirection } from './SlideTransition'; +import { useCalendarState } from './useCalendarState'; +import { makeStyles } from '@material-ui/core/styles'; import { VIEW_HEIGHT } from '../../constants/dimensions'; import { ParsableDate } from '../../constants/prop-types'; import { MaterialUiPickersDate } from '../../typings/date'; import { FadeTransitionGroup } from './FadeTransitionGroup'; import { Calendar, ExportedCalendarProps } from './Calendar'; -import { useUtils, useNow } from '../../_shared/hooks/useUtils'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { useParsedDate } from '../../_shared/hooks/useParsedDate'; +import { useParsedDate } from '../../_shared/hooks/date-helpers-hooks'; import { CalendarHeader, CalendarHeaderProps } from './CalendarHeader'; import { WrapperVariantContext } from '../../wrappers/WrapperVariantContext'; @@ -35,6 +33,8 @@ export interface CalendarViewProps extends ExportedCalendarProps, PublicCalendar views: DatePickerView[]; changeView: (view: DatePickerView) => void; onChange: PickerOnChangeFn; + /** Disable heavy animations @default /(android)/i.test(window.navigator.userAgent) */ + reduceAnimations?: boolean; /** Callback firing on month change. Return promise to render spinner till it will not be resolved @DateIOType */ onMonthChange?: (date: MaterialUiPickersDate) => void | Promise; /** @@ -47,10 +47,6 @@ export interface CalendarViewProps extends ExportedCalendarProps, PublicCalendar * @default Date(2100-01-01) */ maxDate?: ParsableDate; - /** Do not show heavy animations, significantly improves performance on slow devices - * @default /(android)/i.test(navigator.userAgent) - */ - reduceAnimations?: boolean; /** Disable specific date @DateIOType */ shouldDisableDate?: (day: MaterialUiPickersDate) => boolean; /** Callback firing on year change @DateIOType */ @@ -62,76 +58,6 @@ export type ExportedCalendarViewProps = Omit< 'date' | 'view' | 'views' | 'onChange' | 'changeView' | 'slideDirection' | 'currentMonth' >; -type ReducerAction = { type: TType } & TAdditional; - -interface ChangeMonthPayload { - direction: SlideDirection; - newMonth: MaterialUiPickersDate; -} - -interface State { - isMonthSwitchingAnimating: boolean; - loadingQueue: number; - currentMonth: MaterialUiPickersDate; - focusedDay: MaterialUiPickersDate | null; - slideDirection: SlideDirection; -} - -const createCalendarStateReducer = ( - reduceAnimations: boolean, - utils: IUtils -) => ( - state: State, - action: - | ReducerAction<'popLoadingQueue'> - | ReducerAction<'finishMonthSwitchingAnimation'> - | ReducerAction<'changeMonth', ChangeMonthPayload> - | ReducerAction<'changeMonthLoading', ChangeMonthPayload> - | ReducerAction<'changeFocusedDay', { focusedDay: MaterialUiPickersDate }> -): State => { - switch (action.type) { - case 'changeMonthLoading': { - return { - ...state, - loadingQueue: state.loadingQueue + 1, - slideDirection: action.direction, - currentMonth: action.newMonth, - isMonthSwitchingAnimating: !reduceAnimations, - }; - } - case 'changeMonth': { - return { - ...state, - slideDirection: action.direction, - currentMonth: action.newMonth, - isMonthSwitchingAnimating: !reduceAnimations, - }; - } - case 'popLoadingQueue': { - return { - ...state, - loadingQueue: state.loadingQueue <= 0 ? 0 : state.loadingQueue - 1, - }; - } - case 'finishMonthSwitchingAnimation': { - return { - ...state, - isMonthSwitchingAnimating: false, - }; - } - case 'changeFocusedDay': { - const needMonthSwitch = !utils.isSameMonth(state.currentMonth, action.focusedDay); - return { - ...state, - focusedDay: action.focusedDay, - isMonthSwitchingAnimating: needMonthSwitch && !reduceAnimations, - currentMonth: needMonthSwitch ? utils.startOfMonth(action.focusedDay) : state.currentMonth, - slideDirection: utils.isAfterDay(action.focusedDay, state.currentMonth) ? 'left' : 'right', - }; - } - } -}; - export const useStyles = makeStyles( { viewTransitionContainer: { @@ -146,6 +72,9 @@ export const useStyles = makeStyles( { name: 'MuiPickersCalendarView' } ); +export const defaultReduceAnimations = + typeof navigator !== 'undefined' && /(android)/i.test(navigator.userAgent); + export const CalendarView: React.FC = ({ date, view, @@ -154,14 +83,14 @@ export const CalendarView: React.FC = ({ onMonthChange, minDate: unparsedMinDate = new Date('1900-01-01'), maxDate: unparsedMaxDate = new Date('2100-01-01'), - reduceAnimations = typeof window !== 'undefined' && /(android)/i.test(window.navigator.userAgent), + reduceAnimations = defaultReduceAnimations, loadingIndicator = , shouldDisableDate, allowKeyboardControl: allowKeyboardControlProp, + disablePast, + disableFuture, ...other }) => { - const now = useNow(); - const utils = useUtils(); const classes = useStyles(); const minDate = useParsedDate(unparsedMinDate)!; const maxDate = useParsedDate(unparsedMaxDate)!; @@ -169,87 +98,41 @@ export const CalendarView: React.FC = ({ const wrapperVariant = React.useContext(WrapperVariantContext); const allowKeyboardControl = allowKeyboardControlProp ?? wrapperVariant !== 'static'; - const [ - { currentMonth, isMonthSwitchingAnimating, focusedDay, loadingQueue, slideDirection }, - dispatch, - ] = React.useReducer(createCalendarStateReducer(reduceAnimations, utils), { - isMonthSwitchingAnimating: false, - loadingQueue: 0, - focusedDay: date, - currentMonth: utils.startOfMonth(date), - slideDirection: 'left', + const { + loadingQueue, + calendarState, + changeFocusedDay, + changeMonth, + isDateDisabled, + handleChangeMonth, + onMonthSwitchingAnimationEnd, + } = useCalendarState({ + date, + reduceAnimations, + onMonthChange, + minDate, + maxDate, + shouldDisableDate, + disablePast, + disableFuture, }); - const handleChangeMonth = React.useCallback( - (payload: ChangeMonthPayload) => { - const returnedPromise = onMonthChange && onMonthChange(payload.newMonth); - - if (returnedPromise) { - dispatch({ - type: 'changeMonthLoading', - ...payload, - }); - - returnedPromise.then(() => dispatch({ type: 'popLoadingQueue' })); - } else { - dispatch({ - type: 'changeMonth', - ...payload, - }); - } - }, - [onMonthChange] - ); - - const changeMonth = React.useCallback( - (date: MaterialUiPickersDate) => { - if (utils.isSameMonth(date, currentMonth)) { - return; - } - - handleChangeMonth({ - newMonth: utils.startOfMonth(date), - direction: utils.isAfterDay(date, currentMonth) ? 'left' : 'right', - }); - }, - [currentMonth, handleChangeMonth, utils] - ); - React.useEffect(() => { changeMonth(date); }, [date]); // eslint-disable-line React.useEffect(() => { if (view === 'date') { - dispatch({ type: 'changeFocusedDay', focusedDay: date }); + changeFocusedDay(date); } }, [view]); // eslint-disable-line - const validateMinMaxDate = React.useCallback( - (day: MaterialUiPickersDate) => { - return Boolean( - (other.disableFuture && utils.isAfterDay(day, now)) || - (other.disablePast && utils.isBeforeDay(day, now)) || - (minDate && utils.isBeforeDay(day, minDate)) || - (maxDate && utils.isAfterDay(day, maxDate)) - ); - }, - [maxDate, minDate, now, other.disableFuture, other.disablePast, utils] - ); - - const isDateDisabled = React.useCallback( - (day: MaterialUiPickersDate) => { - return validateMinMaxDate(day) || Boolean(shouldDisableDate && shouldDisableDate(day)); - }, - [shouldDisableDate, validateMinMaxDate] - ); - return ( <> handleChangeMonth({ newMonth, direction })} minDate={minDate} @@ -299,15 +182,10 @@ export const CalendarView: React.FC = ({ ) : ( - dispatch({ type: 'finishMonthSwitchingAnimation' }) - } - focusedDay={focusedDay} - changeFocusedDay={focusedDay => dispatch({ type: 'changeFocusedDay', focusedDay })} + {...calendarState} + onMonthSwitchingAnimationEnd={onMonthSwitchingAnimationEnd} + changeFocusedDay={changeFocusedDay} reduceAnimations={reduceAnimations} - currentMonth={currentMonth} - slideDirection={slideDirection} date={date} onChange={onChange} minDate={minDate} diff --git a/lib/src/views/Calendar/Day.tsx b/lib/src/views/Calendar/Day.tsx index e6e843704..d0ea28e87 100644 --- a/lib/src/views/Calendar/Day.tsx +++ b/lib/src/views/Calendar/Day.tsx @@ -1,20 +1,24 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import clsx from 'clsx'; -import { makeStyles, fade } from '@material-ui/core/styles'; import ButtonBase, { ButtonBaseProps } from '@material-ui/core/ButtonBase'; import { ExtendMui } from '../../typings/helpers'; +import { onSpaceOrEnter } from '../../_helpers/utils'; import { useUtils } from '../../_shared/hooks/useUtils'; import { MaterialUiPickersDate } from '../../typings/date'; +import { makeStyles, fade } from '@material-ui/core/styles'; +import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; +import { FORCE_FINISH_PICKER } from '../../_shared/hooks/usePickerState'; -const daySize = 36; export const useStyles = makeStyles( theme => ({ day: { - width: daySize + 4, - height: daySize + 2, + width: DAY_SIZE, + height: DAY_SIZE, borderRadius: '50%', padding: 0, + // background required here to prevent collides with the other days when animating with transition group + backgroundColor: theme.palette.background.paper, color: theme.palette.text.primary, fontSize: theme.typography.caption.fontSize, fontWeight: theme.typography.fontWeightMedium, @@ -30,9 +34,7 @@ export const useStyles = makeStyles( }, }, dayWithMargin: { - margin: '1px 2px', - width: daySize, - height: daySize, + margin: `0px ${DAY_MARGIN}px`, }, dayOutsideMonth: { color: theme.palette.text.hint, @@ -77,11 +79,11 @@ export interface DayProps extends ExtendMui { /** Can be focused by tabbing in */ focusable?: boolean; /** Is day in current month */ - isInCurrentMonth: boolean; + inCurrentMonth: boolean; /** Is switching month animation going on right now */ isAnimating?: boolean; /** Is today? */ - isToday?: boolean; + today?: boolean; /** Disabled? */ disabled?: boolean; /** Selected? */ @@ -99,19 +101,26 @@ export interface DayProps extends ExtendMui { * @default false */ disableHighlightToday?: boolean; + onDayFocus: (day: MaterialUiPickersDate) => void; + onDaySelect: (day: MaterialUiPickersDate, isFinish: boolean | symbol) => void; } -export const Day: React.FC = ({ +const PureDay: React.FC = ({ className, day, disabled, - isInCurrentMonth, - isToday, + hidden, + inCurrentMonth: isInCurrentMonth, + today: isToday, selected, focused = false, focusable = false, isAnimating, + onDayFocus, + onDaySelect, onFocus, + onClick, + onKeyDown, disableMargin = false, allowKeyboardControl, disableHighlightToday = false, @@ -135,9 +144,36 @@ export const Day: React.FC = ({ } }, [allowKeyboardControl, disabled, focused, isAnimating, isInCurrentMonth]); + const handleFocus = (e: React.FocusEvent) => { + if (!focused) { + onDayFocus(day); + } + + if (onFocus) { + onFocus(e); + } + }; + + const handleClick = (e: React.MouseEvent) => { + if (!disabled) { + onDaySelect(day, true); + } + + if (onClick) { + onClick(e); + } + }; + + const handleKeyDown = onSpaceOrEnter(() => { + if (!disabled) { + onDaySelect(day, FORCE_FINISH_PICKER); + } + }, onKeyDown); + + const isHidden = !isInCurrentMonth && !showDaysOutsideCurrentMonth; return ( = ({ [classes.dayDisabled]: disabled, [classes.dayWithMargin]: !disableMargin, [classes.today]: !disableHighlightToday && isToday, - [classes.hidden]: !isInCurrentMonth && !showDaysOutsideCurrentMonth, + [classes.hidden]: isHidden, [classes.dayOutsideMonth]: !isInCurrentMonth && showDaysOutsideCurrentMonth, }, className )} - onFocus={e => { - if (!focused && onFocus) { - onFocus(e); - } - }} {...other} + onFocus={handleFocus} + onKeyDown={handleKeyDown} + onClick={handleClick} > {utils.format(day, 'dayOfMonth')} ); }; -Day.displayName = 'Day'; +export const areDayPropsEqual = (prevProps: DayProps, nextProps: DayProps) => { + return ( + prevProps.focused === nextProps.focused && + prevProps.focusable === nextProps.focusable && + prevProps.isAnimating === nextProps.isAnimating && + prevProps.today === nextProps.today && + prevProps.disabled === nextProps.disabled && + prevProps.selected === nextProps.selected && + prevProps.allowKeyboardControl === nextProps.allowKeyboardControl && + prevProps.disableMargin === nextProps.disableMargin && + prevProps.showDaysOutsideCurrentMonth === nextProps.showDaysOutsideCurrentMonth && + prevProps.disableHighlightToday === nextProps.disableHighlightToday && + prevProps.className === nextProps.className && + prevProps.onDayFocus === nextProps.onDayFocus && + prevProps.onDaySelect === nextProps.onDaySelect + ); +}; + +export const Day = React.memo(PureDay, areDayPropsEqual); + +PureDay.displayName = 'Day'; -Day.propTypes = { - isToday: PropTypes.bool, +PureDay.propTypes = { + today: PropTypes.bool, disabled: PropTypes.bool, - hidden: PropTypes.bool, selected: PropTypes.bool, }; -Day.defaultProps = { +PureDay.defaultProps = { disabled: false, - hidden: false, - isToday: false, + today: false, selected: false, }; diff --git a/lib/src/views/Calendar/DayWrapper.tsx b/lib/src/views/Calendar/DayWrapper.tsx deleted file mode 100644 index d29cc6bcd..000000000 --- a/lib/src/views/Calendar/DayWrapper.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; -import { onSpaceOrEnter } from '../../_helpers/utils'; -import { MaterialUiPickersDate } from '../../typings/date'; -import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { FORCE_FINISH_PICKER } from '../../_shared/hooks/usePickerState'; - -export interface DayWrapperProps { - value: MaterialUiPickersDate; - children: React.ReactNode; - dayInCurrentMonth?: boolean; - disabled?: boolean; - onSelect: PickerOnChangeFn; -} - -const DayWrapper: React.FC = ({ - children, - value, - disabled, - onSelect, - dayInCurrentMonth, - ...other -}) => { - const handleSelection = (isFinish: symbol | boolean) => { - if (!disabled) { - onSelect(value, isFinish); - } - }; - - return ( -
handleSelection(true)} - onKeyDown={onSpaceOrEnter(() => handleSelection(FORCE_FINISH_PICKER))} - children={children} - {...other} - /> - ); -}; - -export default DayWrapper; diff --git a/lib/src/views/Calendar/SlideTransition.tsx b/lib/src/views/Calendar/SlideTransition.tsx index e8a7bc06c..e6020c007 100644 --- a/lib/src/views/Calendar/SlideTransition.tsx +++ b/lib/src/views/Calendar/SlideTransition.tsx @@ -5,7 +5,7 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { CSSTransitionProps } from 'react-transition-group/CSSTransition'; export type SlideDirection = 'right' | 'left'; -interface SlideTransitionProps extends Omit { +export interface SlideTransitionProps extends Omit { transKey: React.Key; className?: string; reduceAnimations: boolean; @@ -36,10 +36,12 @@ export const useStyles = makeStyles( 'slideEnter-left': { willChange: 'transform', transform: 'translate(100%)', + zIndex: 1, }, 'slideEnter-right': { willChange: 'transform', transform: 'translate(-100%)', + zIndex: 1, }, slideEnterActive: { transform: 'translate(0%)', @@ -50,20 +52,22 @@ export const useStyles = makeStyles( }, 'slideExitActiveLeft-left': { willChange: 'transform', - transform: 'translate(-200%)', + transform: 'translate(-100%)', transition: slideTransition, + zIndex: 0, }, 'slideExitActiveLeft-right': { willChange: 'transform', - transform: 'translate(200%)', + transform: 'translate(100%)', transition: slideTransition, + zIndex: 0, }, }; }, { name: 'MuiPickersSlideTransition' } ); -const SlideTransition: React.SFC = ({ +export const SlideTransition: React.SFC = ({ children, transKey, reduceAnimations, @@ -106,5 +110,3 @@ const SlideTransition: React.SFC = ({ ); }; - -export default SlideTransition; diff --git a/lib/src/views/Calendar/Year.tsx b/lib/src/views/Calendar/Year.tsx index e951489e5..1c0f7798a 100644 --- a/lib/src/views/Calendar/Year.tsx +++ b/lib/src/views/Calendar/Year.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import clsx from 'clsx'; import Typography from '@material-ui/core/Typography'; -import { makeStyles, fade } from '@material-ui/core/styles'; import { onSpaceOrEnter } from '../../_helpers/utils'; +import { makeStyles, fade } from '@material-ui/core/styles'; import { WrapperVariantContext } from '../../wrappers/WrapperVariantContext'; export interface YearProps { diff --git a/lib/src/views/Calendar/YearSelection.tsx b/lib/src/views/Calendar/YearSelection.tsx index e85cbf2ff..fee4504ee 100644 --- a/lib/src/views/Calendar/YearSelection.tsx +++ b/lib/src/views/Calendar/YearSelection.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import Year from './Year'; import { DateType } from '@date-io/type'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; import { useUtils } from '../../_shared/hooks/useUtils'; import { MaterialUiPickersDate } from '../../typings/date'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; import { WrapperVariantContext } from '../../wrappers/WrapperVariantContext'; import { useGlobalKeyDown, keycode as keys } from '../../_shared/hooks/useKeyDown'; diff --git a/lib/src/views/Calendar/useCalendarState.tsx b/lib/src/views/Calendar/useCalendarState.tsx new file mode 100644 index 000000000..ee89d2f33 --- /dev/null +++ b/lib/src/views/Calendar/useCalendarState.tsx @@ -0,0 +1,191 @@ +import * as React from 'react'; +import { CalendarViewProps } from './CalendarView'; +import { SlideDirection } from './SlideTransition'; +import { MaterialUiPickersDate } from '../../typings/date'; +import { MuiPickersAdapter, useUtils, useNow } from '../../_shared/hooks/useUtils'; + +interface State { + isMonthSwitchingAnimating: boolean; + loadingQueue: number; + currentMonth: MaterialUiPickersDate; + focusedDay: MaterialUiPickersDate | null; + slideDirection: SlideDirection; +} + +type ReducerAction = { type: TType } & TAdditional; + +interface ChangeMonthPayload { + direction: SlideDirection; + newMonth: MaterialUiPickersDate; +} + +export const createCalendarStateReducer = ( + reduceAnimations: boolean, + disableSwitchToMonthOnDayFocus: boolean, + utils: MuiPickersAdapter +) => ( + state: State, + action: + | ReducerAction<'popLoadingQueue'> + | ReducerAction<'finishMonthSwitchingAnimation'> + | ReducerAction<'changeMonth', ChangeMonthPayload> + | ReducerAction<'changeMonthLoading', ChangeMonthPayload> + | ReducerAction<'changeFocusedDay', { focusedDay: MaterialUiPickersDate }> +): State => { + switch (action.type) { + case 'changeMonthLoading': { + return { + ...state, + loadingQueue: state.loadingQueue + 1, + slideDirection: action.direction, + currentMonth: action.newMonth, + isMonthSwitchingAnimating: !reduceAnimations, + }; + } + case 'changeMonth': { + return { + ...state, + slideDirection: action.direction, + currentMonth: action.newMonth, + isMonthSwitchingAnimating: !reduceAnimations, + }; + } + case 'popLoadingQueue': { + return { + ...state, + loadingQueue: state.loadingQueue <= 0 ? 0 : state.loadingQueue - 1, + }; + } + case 'finishMonthSwitchingAnimation': { + return { + ...state, + isMonthSwitchingAnimating: false, + }; + } + case 'changeFocusedDay': { + const needMonthSwitch = + !disableSwitchToMonthOnDayFocus && + !utils.isSameMonth(state.currentMonth, action.focusedDay); + return { + ...state, + focusedDay: action.focusedDay, + isMonthSwitchingAnimating: needMonthSwitch && !reduceAnimations, + currentMonth: needMonthSwitch ? utils.startOfMonth(action.focusedDay) : state.currentMonth, + slideDirection: utils.isAfterDay(action.focusedDay, state.currentMonth) ? 'left' : 'right', + }; + } + } +}; + +type CalendarStateInput = Pick< + CalendarViewProps, + | 'disableFuture' + | 'disablePast' + | 'shouldDisableDate' + | 'date' + | 'reduceAnimations' + | 'onMonthChange' +> & { + minDate: MaterialUiPickersDate; + maxDate: MaterialUiPickersDate; + disableSwitchToMonthOnDayFocus?: boolean; +}; + +export function useCalendarState({ + date, + reduceAnimations, + onMonthChange, + disablePast, + disableFuture, + minDate, + maxDate, + shouldDisableDate, + disableSwitchToMonthOnDayFocus = false, +}: CalendarStateInput) { + const now = useNow(); + const utils = useUtils(); + const reducerFn = React.useRef( + createCalendarStateReducer(Boolean(reduceAnimations), disableSwitchToMonthOnDayFocus, utils) + ); + const [{ loadingQueue, ...calendarState }, dispatch] = React.useReducer(reducerFn.current, { + isMonthSwitchingAnimating: false, + loadingQueue: 0, + focusedDay: date, + currentMonth: utils.startOfMonth(date), + slideDirection: 'left', + }); + + const handleChangeMonth = React.useCallback( + (payload: ChangeMonthPayload) => { + const returnedPromise = onMonthChange && onMonthChange(payload.newMonth); + + if (returnedPromise) { + dispatch({ + type: 'changeMonthLoading', + ...payload, + }); + + returnedPromise.then(() => dispatch({ type: 'popLoadingQueue' })); + } else { + dispatch({ + type: 'changeMonth', + ...payload, + }); + } + }, + [onMonthChange] + ); + + const changeMonth = React.useCallback( + (date: MaterialUiPickersDate) => { + if (utils.isSameMonth(date, calendarState.currentMonth)) { + return; + } + + handleChangeMonth({ + newMonth: utils.startOfMonth(date), + direction: utils.isAfterDay(date, calendarState.currentMonth) ? 'left' : 'right', + }); + }, + [calendarState.currentMonth, handleChangeMonth, utils] + ); + + const validateMinMaxDate = React.useCallback( + (day: MaterialUiPickersDate) => { + return Boolean( + (disableFuture && utils.isAfterDay(day, now)) || + (disablePast && utils.isBeforeDay(day, now)) || + (minDate && utils.isBeforeDay(day, minDate)) || + (maxDate && utils.isAfterDay(day, maxDate)) + ); + }, + [disableFuture, disablePast, maxDate, minDate, now, utils] + ); + + const isDateDisabled = React.useCallback( + (day: MaterialUiPickersDate) => { + return validateMinMaxDate(day) || Boolean(shouldDisableDate && shouldDisableDate(day)); + }, + [shouldDisableDate, validateMinMaxDate] + ); + + const onMonthSwitchingAnimationEnd = React.useCallback(() => { + dispatch({ type: 'finishMonthSwitchingAnimation' }); + }, []); + + const changeFocusedDay = React.useCallback( + (newFocusedDate: MaterialUiPickersDate) => + dispatch({ type: 'changeFocusedDay', focusedDay: newFocusedDate }), + [] + ); + + return { + loadingQueue, + calendarState, + changeMonth, + changeFocusedDay, + isDateDisabled, + onMonthSwitchingAnimationEnd, + handleChangeMonth, + }; +} diff --git a/lib/src/views/Clock/ClockView.tsx b/lib/src/views/Clock/ClockView.tsx index a6d54af52..bded0faf2 100644 --- a/lib/src/views/Clock/ClockView.tsx +++ b/lib/src/views/Clock/ClockView.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { makeStyles } from '@material-ui/core/styles'; import Clock from './Clock'; import { pipe } from '../../_helpers/utils'; +import { makeStyles } from '@material-ui/core/styles'; import { useUtils } from '../../_shared/hooks/useUtils'; import { ParsableDate } from '../../constants/prop-types'; import { MaterialUiPickersDate } from '../../typings/date'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { useParsedDate } from '../../_shared/hooks/useParsedDate'; import { getHourNumbers, getMinutesNumbers } from './ClockNumbers'; import { useMeridiemMode } from '../../TimePicker/TimePickerToolbar'; +import { useParsedDate } from '../../_shared/hooks/date-helpers-hooks'; import { ArrowSwitcher, ExportedArrowSwitcherProps } from '../../_shared/ArrowSwitcher'; import { convertValueToMeridiem, createIsAfterIgnoreDatePart } from '../../_helpers/time-utils'; diff --git a/lib/src/views/MobileKeyboardInputView.tsx b/lib/src/views/MobileKeyboardInputView.tsx index 2ff95dd69..127c98ba0 100644 --- a/lib/src/views/MobileKeyboardInputView.tsx +++ b/lib/src/views/MobileKeyboardInputView.tsx @@ -1,27 +1,5 @@ -import * as React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import KeyboardDateInput from '../_shared/KeyboardDateInput'; -import { DateInputProps } from '../_shared/PureDateInput'; -import { InnerMobileWrapperProps } from '../wrappers/MobileWrapper'; +import { styled } from '@material-ui/core/styles'; -interface MobileKeyboardInputViewProps extends DateInputProps, Partial {} - -const useStyles = makeStyles(() => ({ - mobileKeyboardView: { - padding: '16px 24px', - }, -})); - -export const MobileKeyboardInputView: React.FC = ({ - clearLabel, - DialogProps, - clearable, - ...other -}) => { - const classes = useStyles(); - return ( -
- -
- ); -}; +export const MobileKeyboardInputView = styled('div')({ + padding: '16px 24px', +}); diff --git a/lib/src/wrappers/DesktopPopperWrapper.tsx b/lib/src/wrappers/DesktopPopperWrapper.tsx new file mode 100644 index 000000000..509dbc9d6 --- /dev/null +++ b/lib/src/wrappers/DesktopPopperWrapper.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import Grow from '@material-ui/core/Grow'; +import Paper from '@material-ui/core/Paper'; +// @ts-ignore TODO make definitions +import TrapFocus from '@material-ui/core/Modal/TrapFocus'; +import Popper, { PopperProps } from '@material-ui/core/Popper'; +import { WrapperProps } from './Wrapper'; +import { StaticWrapperProps } from './StaticWrapper'; +import { makeStyles } from '@material-ui/core/styles'; +import { InnerMobileWrapperProps } from './MobileWrapper'; +import { InnerDesktopWrapperProps } from './DesktopWrapper'; +import { WrapperVariantContext } from './WrapperVariantContext'; +import { KeyboardDateInput } from '../_shared/KeyboardDateInput'; +import { useGlobalKeyDown, keycode } from '../_shared/hooks/useKeyDown'; +import { TransitionProps } from '@material-ui/core/transitions/transition'; +import { executeInTheNextEventLoopTick, createDelegatedEventHandler } from '../_helpers/utils'; + +export interface InnerDesktopPopperWrapperProps { + /** Popper props passed to material-ui [Popper](https://material-ui.com/api/popper/#popper-api) */ + PopperProps?: Partial; + /** Custom component for [transition](https://material-ui.com/components/transitions/#transitioncomponent-prop) */ + TransitionComponent?: React.ComponentType; +} + +export interface DesktopPopperWrapperProps + extends InnerDesktopPopperWrapperProps, + WrapperProps, + Partial {} + +const useStyles = makeStyles(theme => ({ + popper: { + zIndex: theme.zIndex.modal, + }, + paper: { + transformOrigin: 'top center', + '&:focus': { + outline: 'auto', + '@media (pointer:coarse)': { + outline: 0, + }, + }, + }, + topTransition: { + transformOrigin: 'bottom center', + }, +})); + +export const DesktopPopperWrapper: React.FC = ({ + open, + wider, + children, + PopperProps, + PopoverProps, + onClear, + onDismiss, + onSetToday, + onAccept, + showTabs, + DateInputProps, + okLabel, + cancelLabel, + clearLabel, + todayLabel, + showTodayButton, + clearable, + DialogProps, + PureDateInputComponent, + displayStaticWrapperAs, + TransitionComponent = Grow, + KeyboardDateInputComponent = KeyboardDateInput, + ...other +}) => { + const classes = useStyles(); + const inputRef = React.useRef(null); + const popperRef = React.useRef(null); + + useGlobalKeyDown(open, { + [keycode.Esc]: onDismiss, + }); + + const handleBlur = () => { + executeInTheNextEventLoopTick(() => { + if ( + inputRef.current?.contains(document.activeElement) || + popperRef.current?.contains(document.activeElement) + ) { + return; + } + + onDismiss(); + }); + }; + + return ( + + + + + {({ TransitionProps, placement }) => ( + true} + getDoc={() => popperRef.current?.ownerDocument ?? document} + > + + + {children} + + + + )} + + + ); +}; diff --git a/lib/src/wrappers/DesktopWrapper.tsx b/lib/src/wrappers/DesktopWrapper.tsx index 7c698a7c2..4fedd922a 100644 --- a/lib/src/wrappers/DesktopWrapper.tsx +++ b/lib/src/wrappers/DesktopWrapper.tsx @@ -1,11 +1,13 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import Popover, { PopoverProps } from '@material-ui/core/Popover'; -import { makeStyles } from '@material-ui/core/styles'; import KeyboardDateInput from '../_shared/KeyboardDateInput'; +import Popover, { PopoverProps } from '@material-ui/core/Popover'; import { WrapperProps } from './Wrapper'; +import { StaticWrapperProps } from './StaticWrapper'; +import { makeStyles } from '@material-ui/core/styles'; import { InnerMobileWrapperProps } from './MobileWrapper'; import { WrapperVariantContext } from './WrapperVariantContext'; +import { InnerDesktopPopperWrapperProps } from './DesktopPopperWrapper'; export interface InnerDesktopWrapperProps { /** Popover props passed to material-ui Popover */ @@ -15,7 +17,7 @@ export interface InnerDesktopWrapperProps { export interface DesktopWrapperProps extends InnerDesktopWrapperProps, WrapperProps, - Partial {} + Partial {} const useStyles = makeStyles({ popover: { @@ -33,6 +35,8 @@ export const DesktopWrapper: React.FC = ({ wider, children, PopoverProps, + PopperProps, + TransitionComponent, onClear, onDismiss, onSetToday, @@ -47,6 +51,7 @@ export const DesktopWrapper: React.FC = ({ clearable, DialogProps, PureDateInputComponent, + displayStaticWrapperAs, KeyboardDateInputComponent = KeyboardDateInput, ...other }) => { diff --git a/lib/src/wrappers/MobileWrapper.tsx b/lib/src/wrappers/MobileWrapper.tsx index 1b98efba2..4a00642ec 100644 --- a/lib/src/wrappers/MobileWrapper.tsx +++ b/lib/src/wrappers/MobileWrapper.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; -import { DialogProps as MuiDialogProps } from '@material-ui/core/Dialog'; import ModalDialog from '../_shared/ModalDialog'; import { WrapperProps } from './Wrapper'; +import { StaticWrapperProps } from './StaticWrapper'; import { PureDateInput } from '../_shared/PureDateInput'; import { InnerDesktopWrapperProps } from './DesktopWrapper'; import { WrapperVariantContext } from './WrapperVariantContext'; +import { DialogProps as MuiDialogProps } from '@material-ui/core/Dialog'; export interface InnerMobileWrapperProps { /** @@ -50,7 +51,7 @@ export interface InnerMobileWrapperProps { export interface MobileWrapperProps extends InnerMobileWrapperProps, WrapperProps, - Partial {} + Partial {} export const MobileWrapper: React.FC = ({ open, @@ -70,13 +71,14 @@ export const MobileWrapper: React.FC = ({ onDismiss, onSetToday, PopoverProps, + displayStaticWrapperAs, KeyboardDateInputComponent, PureDateInputComponent = PureDateInput, ...other }) => { return ( - + = ({ - desktopModeBreakpoint = 'md', - okLabel, - cancelLabel, - clearLabel, - todayLabel, - showTodayButton, - clearable, - DialogProps, - PopoverProps, - ...other -}) => { - const isDesktop = useMediaQuery(theme => theme.breakpoints.up(desktopModeBreakpoint)); +export const makeResponsiveWrapper = ( + DesktopWrapperComponent: React.FC, + MobileWrapperComponent: React.FC +) => { + const ResponsiveWrapper: React.FC = ({ + desktopModeBreakpoint = 'md', + okLabel, + cancelLabel, + clearLabel, + todayLabel, + showTodayButton, + clearable, + DialogProps, + PopoverProps, + PopperProps, + TransitionComponent, + displayStaticWrapperAs, + ...other + }) => { + const theme = useTheme(); + const isDesktop = useMediaQuery(theme.breakpoints.up(desktopModeBreakpoint)); - return isDesktop ? ( - - ) : ( - - ); + return isDesktop ? ( + + ) : ( + + ); + }; + + return ResponsiveWrapper; }; + +export const ResponsiveWrapper = makeResponsiveWrapper(DesktopWrapper, MobileWrapper); + +export const ResponsivePopperWrapper = makeResponsiveWrapper(DesktopPopperWrapper, MobileWrapper); diff --git a/lib/src/wrappers/StaticWrapper.tsx b/lib/src/wrappers/StaticWrapper.tsx index 073504824..0e110ad00 100644 --- a/lib/src/wrappers/StaticWrapper.tsx +++ b/lib/src/wrappers/StaticWrapper.tsx @@ -16,11 +16,22 @@ const useStyles = makeStyles( { name: 'MuiPickersStaticWrapper' } ); -export const StaticWrapper: React.FC = ({ children }) => { +export interface StaticWrapperProps { + /** + * Force static wrapper inner components to be rendered in mobile or desktop mode + * @default "static" + */ + displayStaticWrapperAs?: 'desktop' | 'mobile' | 'static'; +} + +export const StaticWrapper: React.FC = ({ + displayStaticWrapperAs = 'static', + children, +}) => { const classes = useStyles(); return ( - +
); diff --git a/lib/src/wrappers/Wrapper.tsx b/lib/src/wrappers/Wrapper.tsx index 8f92b0c56..0d24ccecb 100644 --- a/lib/src/wrappers/Wrapper.tsx +++ b/lib/src/wrappers/Wrapper.tsx @@ -1,8 +1,9 @@ -import { StaticWrapper } from './StaticWrapper'; import { DateInputProps } from '../_shared/PureDateInput'; +import { StaticWrapper, StaticWrapperProps } from './StaticWrapper'; import { MobileWrapper, MobileWrapperProps } from './MobileWrapper'; import { DesktopWrapper, DesktopWrapperProps } from './DesktopWrapper'; import { ResponsiveWrapper, ResponsiveWrapperProps } from './ResponsiveWrapper'; +import { DesktopPopperWrapper, DesktopPopperWrapperProps } from './DesktopPopperWrapper'; export interface WrapperProps> { open: boolean; @@ -15,22 +16,27 @@ export interface WrapperProps> { PureDateInputComponent?: React.ComponentType; } -export type OmitInnerWrapperProps> = Omit; +export type OmitInnerWrapperProps> = Omit>; export type SomeWrapper = | typeof ResponsiveWrapper | typeof StaticWrapper | typeof MobileWrapper - | typeof DesktopWrapper; + | typeof DesktopWrapper + | typeof DesktopPopperWrapper; export type ExtendWrapper = TWrapper extends typeof StaticWrapper - ? {} // no additional props + ? StaticWrapperProps : TWrapper extends typeof ResponsiveWrapper ? OmitInnerWrapperProps : TWrapper extends typeof MobileWrapper ? OmitInnerWrapperProps : TWrapper extends typeof DesktopWrapper ? OmitInnerWrapperProps + : TWrapper extends typeof DesktopWrapper + ? OmitInnerWrapperProps + : TWrapper extends typeof DesktopPopperWrapper + ? OmitInnerWrapperProps : never; export function getWrapperVariant(wrapper: SomeWrapper) { diff --git a/lib/src/wrappers/makeWrapperComponent.tsx b/lib/src/wrappers/makeWrapperComponent.tsx index 12aab4a3f..c45efebd2 100644 --- a/lib/src/wrappers/makeWrapperComponent.tsx +++ b/lib/src/wrappers/makeWrapperComponent.tsx @@ -1,31 +1,37 @@ import React from 'react'; +import { StaticWrapperProps } from './StaticWrapper'; import { BasePickerProps } from '../typings/BasePicker'; import { DateInputProps } from '../_shared/PureDateInput'; import { ResponsiveWrapperProps } from './ResponsiveWrapper'; import { DateValidationProps } from '../_helpers/text-field-helper'; import { OmitInnerWrapperProps, SomeWrapper, WrapperProps } from './Wrapper'; -interface MakePickerOptions { - PureDateInputComponent?: React.FC>; - KeyboardDateInputComponent?: React.FC>; +interface MakePickerOptions { + PureDateInputComponent?: React.FC; + KeyboardDateInputComponent?: React.FC; } -interface WithWrapperProps { +interface WithWrapperProps { children: React.ReactNode; - inputProps: DateInputProps; + inputProps: TInputProps; wrapperProps: Omit; } /** Creates a component that rendering modal/popover/nothing and spreading props down to text field */ -export function makeWrapperComponent( +export function makeWrapperComponent< + TInputProps extends DateInputProps, + TInputValue, + TDateValue, + TWrapper extends SomeWrapper = any +>( Wrapper: TWrapper, - { KeyboardDateInputComponent, PureDateInputComponent }: MakePickerOptions + { KeyboardDateInputComponent, PureDateInputComponent }: MakePickerOptions ) { function WrapperComponent( props: Partial> & DateValidationProps & - WithWrapperProps & - Partial> + WithWrapperProps & + Partial & StaticWrapperProps> ) { const { open, @@ -53,6 +59,7 @@ export function makeWrapperComponent diff --git a/lib/typings.d.ts b/lib/typings.d.ts index e9750fd65..e513b388b 100644 --- a/lib/typings.d.ts +++ b/lib/typings.d.ts @@ -1,6 +1,3 @@ declare module '@date-io/type' { - import { Moment } from 'moment'; - import { DateTime } from 'luxon'; - - export type DateType = Moment | DateTime | Date; + export type DateType = unknown; } diff --git a/package.json b/package.json index df5325bde..39ecaec23 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "husky": "^4.2.3", "lint-staged": "^10.0.8", "prettier": "^1.14.3", + "ts-loader": "^6.2.2", "wait-on": "^4.0.0" }, "husky": { diff --git a/yarn.lock b/yarn.lock index 32421057a..7dae6f2b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1237,13 +1237,20 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308" integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ== dependencies: regenerator-runtime "^0.13.2" +"@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7": + version "7.9.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" + integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.1.2", "@babel/template@^7.6.0", "@babel/template@^7.7.0": version "7.7.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.7.0.tgz#4fadc1b8e734d97f56de39c77de76f2562e597d0" @@ -1353,52 +1360,52 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@date-io/core@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.4.0.tgz#59e1c6de1d48ee63c6e97681b415076d04e7b5bf" - integrity sha512-XUr4TSwFmthcCn5QYnGqobbnBqOsSyCggRfvieMQHPSz5zei8KYpw4xlvFFQfu/MI3CmCHDjWMkVaPy/uFIDNA== +"@date-io/core@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.5.0.tgz#afb3a82e989a925755cba139b71f95b6b396dce2" + integrity sha512-GifWlc0hyLdYwivltV8KVwE+OOVgYoHF4DvvKn6VOA73iVvqxbXXeL18PVFjMw8r0JUHmuhP6S+4TD8INBzXZA== "@date-io/date-fns@^2.1.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.4.0.tgz#d41aa353806a3b8aa28fbcadf2c860438bcbfaa9" - integrity sha512-DYlfSiTs6GuPmbAmJ9ws7aaOd8f89lVBBqU+71w4Qxxv9DW2qQvBWwOkvCJ5qSp4ae4+PtCzy6JKQDKsdgBaJg== + version "2.5.0" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.5.0.tgz#10ff3b2220f1814e1d4d835dbfb9fb6f497e83aa" + integrity sha512-HiQqjVLFUPsOYtW3damMw5jsY5Yk2KG3LcI7s2eioce+jgxZ6XjhGiyWur14btgTVihhkVjfdzmd5XMfpRXH1Q== dependencies: - "@date-io/core" "^2.4.0" + "@date-io/core" "^2.5.0" "@date-io/dayjs@^2.1.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.4.0.tgz#972094ba29af18422c01e2674a3d5c5704382a76" - integrity sha512-U4IxzpK4Us9lkMN+iTzwJayKjIcb+0X2Kdpt+XoB+0kug7ri+wGwypxncaYnRwwztYGBKWJcQiDlSOBU+0pkBA== + version "2.5.1" + resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.5.1.tgz#a27159390c47cf5755e4053e377e275d0663b66f" + integrity sha512-fUEaDwIc3Dq+UisDHeNTY/5P/BYU4yihh93teW1Z2PYeOYKraTQX639EoDLG5XnA0jsxig6lS7Z3Pw4BBpapKw== dependencies: - "@date-io/core" "^2.4.0" + "@date-io/core" "^2.5.0" "@date-io/hijri@^2.2.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@date-io/hijri/-/hijri-2.4.0.tgz#0deaabd0f3431c357912defd4037e7b717906e58" - integrity sha512-0ZHbKvFCzncdc920wnVfIU1p/So9NNFXT7X0QvP0XzhQIq3qVMYuJsmpaogU20UNZrMQ612yOjuz94HtmdEjUA== + version "2.5.1" + resolved "https://registry.yarnpkg.com/@date-io/hijri/-/hijri-2.5.1.tgz#6bcc20a2097731d8f4e268393946fd3bee3c6797" + integrity sha512-v0Iszo++y2sqlzDQhACBCHr38X01dCV5uDIatwvk1mj3IQoYzhi60imd9BYD6VVaUhwH04mONbC4U2IsYSqsPQ== dependencies: - "@date-io/moment" "^2.4.0" + "@date-io/moment" "^2.5.1" "@date-io/jalaali@^2.0.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@date-io/jalaali/-/jalaali-2.4.0.tgz#58a2a5df813285fe5d3c477d7d1b2a0a2cdd344a" - integrity sha512-f9Uj11F2KUmlp3AA+ocddXaK4Wit5Pc7tUVz90sS8Um1jM2vHb/f6F4d+RhouH0/8DuIFClMGCYPFWRvUjN1YQ== + version "2.5.1" + resolved "https://registry.yarnpkg.com/@date-io/jalaali/-/jalaali-2.5.1.tgz#d22c7a9d748f07d80ab73a5376ee3f1fb5bd919b" + integrity sha512-+/lrgjIARXQcjr9ARRN0bygYeoP43lE95Ec1skeq9P6X4q1eLk0xyhGtsygUNeFxFenrFk1n19Z5d2qC9U/Hsw== dependencies: - "@date-io/moment" "^2.4.0" + "@date-io/moment" "^2.5.1" "@date-io/luxon@^2.1.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.4.0.tgz#b032f1d4eac47c18dc1c447e9235295cf95bf166" - integrity sha512-EFbLSYVhXcxMxg4pJF7e5yCwB9KZc4MJlsWSpXUla29ylcB9rlU07AeV74ogdy+kzakFFN/QdFxhuMvxqyBAxw== + version "2.5.1" + resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.5.1.tgz#75986a0d42a6a7a0de9a6c606bec0773db4d7b48" + integrity sha512-wRxE8/aKgLSsAVC/+joRs4GKswuxEbSQd+D/b2QEBpak0+FUPad/MwnmImcbRIkqV8n5Oc3NZ+JISDGVpgpa7w== dependencies: - "@date-io/core" "^2.4.0" + "@date-io/core" "^2.5.0" -"@date-io/moment@^2.1.0", "@date-io/moment@^2.4.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.4.0.tgz#84612f4cf175ec8093c43a6b6176f874d2d1f007" - integrity sha512-857C9idmZGpD8NygxQsOXPa3OePC6UFAhU996dCUv8cY17p1tkjgj7/JZ3J7PTV2i8ybeUDUD/tAadACEtI05g== +"@date-io/moment@^2.1.0", "@date-io/moment@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.5.1.tgz#6f2c4437f718a51dc0675a97ea2d1d435e9d4b76" + integrity sha512-B7IF8yfEf6dnKX3bFml18IQ8cpToYrkQQPKjscSjVZS72xO5gXW5pRqYZx0pbZbHnpw/WS5Fy/lnHPwTyIWCWA== dependencies: - "@date-io/core" "^2.4.0" + "@date-io/core" "^2.5.0" "@emotion/hash@^0.8.0": version "0.8.0" @@ -2076,10 +2083,17 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== +"@types/jss@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/jss/-/jss-10.0.0.tgz#bea379e2b64d0f0f0f9e9093471036643608c676" + integrity sha512-Obo9w999hf2Q4sU/MRT7iR1UazdvFK3pEdtB6BSfO0NbmDrVMQi+RgNts4DLmlFho5Uf1PG4cMKLT8POhb6utA== + dependencies: + jss "*" + "@types/luxon@^1.11.0": - version "1.21.0" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.21.0.tgz#db792d29f535d49522cb6d94dd9da053efc950a1" - integrity sha512-Zhrf65tpjOlVIYrUhX9eu1VzRo8iixQDLFPbfqFxPpG4pBTNNPZ2BFhYE0IAsDfW9GWg+RcrUqiLwrGJH4rq4w== + version "1.22.0" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.22.0.tgz#dbdf2cc7ba3dfce98c57a3f0e003791122cba009" + integrity sha512-riAvdx85rU7OXCrjW3f7dIf7fuJDrxck2Dkjd0weh6ul7q+wumrwe6+/tD8v7yOKnZAuEnTFF4FU7b+5W/I3bw== "@types/minimatch@*": version "3.0.3" @@ -2178,9 +2192,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^16.8.13", "@types/react@^16.8.2": - version "16.9.17" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.17.tgz#58f0cc0e9ec2425d1441dd7b623421a867aa253e" - integrity sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg== + version "16.9.32" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.32.tgz#f6368625b224604148d1ddf5920e4fefbd98d383" + integrity sha512-fmejdp0CTH00mOJmxUPPbWCEBWPvRIL4m8r0qD+BSDUqmutPyGQCHifzMpMzdvZwROdEdL78IuZItntFWgPXHQ== dependencies: "@types/prop-types" "*" csstype "^2.2.0" @@ -3874,9 +3888,9 @@ cliui@^6.0.0: wrap-ansi "^6.2.0" clsx@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.0.4.tgz#0c0171f6d5cb2fe83848463c15fcc26b4df8c2ec" - integrity sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg== + version "1.1.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" + integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== co@^4.6.0: version "4.6.0" @@ -4150,7 +4164,12 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.4.0, core-js@^2.5.7: +core-js@^2.4.0: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== + +core-js@^2.5.7: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -4322,11 +4341,11 @@ css-vendor@^0.3.8: is-in-browser "^1.0.2" css-vendor@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.7.tgz#4e6d53d953c187981576d6a542acc9fb57174bda" - integrity sha512-VS9Rjt79+p7M0WkPqcAza4Yq1ZHrsHrwf7hPL/bjQB+c1lwmAI+1FXxYTYt818D/50fFVflw0XKleiBN5RITkg== + version "2.0.8" + resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" + integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== dependencies: - "@babel/runtime" "^7.6.2" + "@babel/runtime" "^7.8.3" is-in-browser "^1.0.2" css-what@2.1: @@ -4357,9 +4376,9 @@ cssstyle@^2.0.0: cssom "~0.3.6" csstype@^2.2.0, csstype@^2.5.2, csstype@^2.6.5, csstype@^2.6.7: - version "2.6.8" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" - integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== + version "2.6.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b" + integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w== cyclist@^1.0.1: version "1.0.1" @@ -4673,11 +4692,11 @@ doctrine@^3.0.0: esutils "^2.0.2" dom-helpers@^5.0.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.3.tgz#7233248eb3a2d1f74aafca31e52c5299cc8ce821" - integrity sha512-nZD1OtwfWGRBWlpANxacBEZrEuLa16o1nh7YopFWeoF68Zt8GGEmzHu6Xv4F3XaFIC+YXtTLrzgqKxFgLEe4jw== + version "5.1.4" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b" + integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A== dependencies: - "@babel/runtime" "^7.6.3" + "@babel/runtime" "^7.8.7" csstype "^2.6.7" dom-serializer@0, dom-serializer@^0.2.1: @@ -5094,9 +5113,9 @@ escodegen@^1.11.1: source-map "~0.6.1" eslint-config-prettier@^6.10.0: - version "6.10.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz#7b15e303bf9c956875c948f6b21500e48ded6a7f" - integrity sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg== + version "6.10.1" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz#129ef9ec575d5ddc0e269667bf09defcd898642a" + integrity sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ== dependencies: get-stdin "^6.0.0" @@ -7594,63 +7613,63 @@ jss-nested@^6.0.1: warning "^3.0.0" jss-plugin-camel-case@^10.0.3: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.0.4.tgz#3dedecec1e5bba0bf6141c2c05e2ab11ea4b468d" - integrity sha512-+wnqxJsyfUnOn0LxVg3GgZBSjfBCrjxwx7LFxwVTUih0ceGaXKZoieheNOaTo5EM4w8bt1nbb8XonpQCj67C6A== + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.1.1.tgz#8e73ecc4f1d0f8dfe4dd31f6f9f2782588970e78" + integrity sha512-MDIaw8FeD5uFz1seQBKz4pnvDLnj5vIKV5hXSVdMaAVq13xR6SVTVWkIV/keyTs5txxTvzGJ9hXoxgd1WTUlBw== dependencies: "@babel/runtime" "^7.3.1" hyphenate-style-name "^1.0.3" - jss "10.0.4" + jss "10.1.1" jss-plugin-default-unit@^10.0.3: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.0.4.tgz#df03885de20f20a1fc1c21bdb7c62e865ee400d9" - integrity sha512-T0mhL/Ogp/quvod/jAHEqKvptLDxq7Cj3a+7zRuqK8HxUYkftptN89wJElZC3rshhNKiogkEYhCWenpJdFvTBg== + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.1.1.tgz#2df86016dfe73085eead843f5794e3890e9c5c47" + integrity sha512-UkeVCA/b3QEA4k0nIKS4uWXDCNmV73WLHdh2oDGZZc3GsQtlOCuiH3EkB/qI60v2MiCq356/SYWsDXt21yjwdg== dependencies: "@babel/runtime" "^7.3.1" - jss "10.0.4" + jss "10.1.1" jss-plugin-global@^10.0.3: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.0.4.tgz#412245b56133cc88bec654a70d82d5922619f4c5" - integrity sha512-N8n9/GHENZce+sqE4UYiZiJtI+t+erT/BypHOrNYAfIoNEj7OYsOEKfIo2P0GpLB3QyDAYf5eo9XNdZ8veEkUA== + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.1.1.tgz#36b0d6d9facb74dfd99590643708a89260747d14" + integrity sha512-VBG3wRyi3Z8S4kMhm8rZV6caYBegsk+QnQZSVmrWw6GVOT/Z4FA7eyMu5SdkorDlG/HVpHh91oFN56O4R9m2VA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.0.4" + jss "10.1.1" jss-plugin-nested@^10.0.3: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.0.4.tgz#4d15ad13995fb6e4125618006473a096d2475d75" - integrity sha512-QM21BKVt8LDeoRfowvAMh/s+/89VYrreIIE6ch4pvw0oAXDWw1iorUPlqLZ7uCO3UL0uFtQhJq3QMLN6Lr1v0A== + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.1.1.tgz#5c3de2b8bda344de1ebcef3a4fd30870a29a8a8c" + integrity sha512-ozEu7ZBSVrMYxSDplPX3H82XHNQk2DQEJ9TEyo7OVTPJ1hEieqjDFiOQOxXEj9z3PMqkylnUbvWIZRDKCFYw5Q== dependencies: "@babel/runtime" "^7.3.1" - jss "10.0.4" + jss "10.1.1" tiny-warning "^1.0.2" jss-plugin-props-sort@^10.0.3: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.0.4.tgz#43c880ff8dfcf858f809f663ece5e65a1d945b5a" - integrity sha512-WoETdOCjGskuin/OMt2uEdDPLZF3vfQuHXF+XUHGJrq0BAapoyGQDcv37SeReDlkRAbVXkEZPsIMvYrgHSHFiA== + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.1.1.tgz#34bddcbfaf9430ec8ccdf92729f03bb10caf1785" + integrity sha512-g/joK3eTDZB4pkqpZB38257yD4LXB0X15jxtZAGbUzcKAVUHPl9Jb47Y7lYmiGsShiV4YmQRqG1p2DHMYoK91g== dependencies: "@babel/runtime" "^7.3.1" - jss "10.0.4" + jss "10.1.1" jss-plugin-rule-value-function@^10.0.3: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.0.4.tgz#2f4cf4a86ad3eba875bb48cb9f4a7ed35cb354e7" - integrity sha512-0hrzOSWRF5ABJGaHrlnHbYZjU877Ofzfh2id3uLtBvemGQLHI+ldoL8/+6iPSRa7M8z8Ngfg2vfYhKjUA5gA0g== + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.1.1.tgz#be00dac6fc394aaddbcef5860b9eca6224d96382" + integrity sha512-ClV1lvJ3laU9la1CUzaDugEcwnpjPTuJ0yGy2YtcU+gG/w9HMInD5vEv7xKAz53Bk4WiJm5uLOElSEshHyhKNw== dependencies: "@babel/runtime" "^7.3.1" - jss "10.0.4" + jss "10.1.1" jss-plugin-vendor-prefixer@^10.0.3: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.0.4.tgz#1626ef612a4541cff17cf96815e1740155214ed2" - integrity sha512-4JgEbcrdeMda1qvxTm1CnxFJAWVV++VLpP46HNTrfH7VhVlvUpihnUNs2gAlKuRT/XSBuiWeLAkrTqF4NVrPig== + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.1.1.tgz#8348b20749f790beebab3b6a8f7075b07c2cfcfd" + integrity sha512-09MZpQ6onQrhaVSF6GHC4iYifQ7+4YC/tAP6D4ZWeZotvCMq1mHLqNKRIaqQ2lkgANjlEot2JnVi1ktu4+L4pw== dependencies: "@babel/runtime" "^7.3.1" css-vendor "^2.0.7" - jss "10.0.4" + jss "10.1.1" jss-preset-default@^4.3.0: version "4.5.0" @@ -7694,20 +7713,10 @@ jss-vendor-prefixer@^7.0.0: dependencies: css-vendor "^0.3.8" -jss@10.0.4: - version "10.0.4" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.0.4.tgz#46ebdde1c40c9a079d64f3334cb88ae28fd90bfd" - integrity sha512-GqHmeDK83qbqMAVjxyPfN1qJVTKZne533a9bdCrllZukUM8npG/k+JumEPI86IIB5ifaZAHG2HAsUziyxOiooQ== - dependencies: - "@babel/runtime" "^7.3.1" - csstype "^2.6.5" - is-in-browser "^1.1.3" - tiny-warning "^1.0.2" - -jss@^10.0.0, jss@^10.0.3: - version "10.0.3" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.0.3.tgz#5c160f96aa8ce8b9f851ee0b33505dcd37f490a4" - integrity sha512-AcDvFdOk16If9qvC9KN3oFXsrkHWM9+TaPMpVB9orm3z+nq1Xw3ofHyflRe/mkSucRZnaQtlhZs1hdP3DR9uRw== +jss@*, jss@10.1.1, jss@^10.0.0, jss@^10.0.3: + version "10.1.1" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.1.1.tgz#450b27d53761af3e500b43130a54cdbe157ea332" + integrity sha512-Xz3qgRUFlxbWk1czCZibUJqhVPObrZHxY3FPsjCXhDld4NOj1BgM14Ir5hVm+Qr6OLqVljjGvoMcCdXNOAbdkQ== dependencies: "@babel/runtime" "^7.3.1" csstype "^2.6.5" @@ -8382,12 +8391,12 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -minimist@1.2.0, minimist@^1.1.1, minimist@^1.2.0: +minimist@1.2.0, minimist@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -9454,9 +9463,9 @@ pn@^1.1.0: integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== popper.js@^1.14.1: - version "1.16.0" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" - integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw== + version "1.16.1" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" + integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== posix-character-classes@^0.1.0: version "0.1.1" @@ -9845,14 +9854,14 @@ react-docgen-typescript@^1.16.2: integrity sha512-nECrg2qih81AKp0smkxXebF72/2EjmEn7gXSlWLDHLbpGcbw2yIorol24fw1FWqvndIY82sfSd0x/SyfMKY1Jw== react-dom@^16.13.0: - version "16.13.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.0.tgz#cdde54b48eb9e8a0ca1b3dc9943d9bb409b81866" - integrity sha512-y09d2c4cG220DzdlFkPTnVvGTszVvNpC73v+AaLGLHbkpy3SSgvYq8x0rNwPJ/Rk/CicTNgk0hbHNw1gMEZAXg== + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" + integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.19.0" + scheduler "^0.19.1" react-error-overlay@5.1.4: version "5.1.4" @@ -9869,11 +9878,16 @@ react-is@16.6.3: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0" integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA== -react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.12.0, react-is@^16.8.6, react-is@^16.9.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== +react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + react-jss@^8.6.1: version "8.6.1" resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.6.1.tgz#a06e2e1d2c4d91b4d11befda865e6c07fbd75252" @@ -9943,9 +9957,9 @@ react-transition-group@^4.0.0, react-transition-group@^4.3.0: prop-types "^15.6.2" react@^16.13.0: - version "16.13.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.0.tgz#d046eabcdf64e457bbeed1e792e235e1b9934cf7" - integrity sha512-TSavZz2iSLkq5/oiE7gnFzmURKZMltmi193rm5HEoUDAXpzT9Kzw6oNZnGoai/4+fUnm7FqS5dwgUL34TujcWQ== + version "16.13.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" + integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -10083,12 +10097,12 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== -regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3: +regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== -regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4: version "0.13.5" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== @@ -10612,10 +10626,10 @@ scheduler@^0.18.0: loose-envify "^1.1.0" object-assign "^4.1.1" -scheduler@^0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.0.tgz#a715d56302de403df742f4a9be11975b32f5698d" - integrity sha512-xowbVaTPe9r7y7RUejcK73/j8tt2jfiyTednOvHbA8JoClvMYCp+r8QegLwK/n8zWQAtZb1fFnER4XLBZXrCxA== +scheduler@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -11728,6 +11742,17 @@ ts-loader@^6.0.2: micromatch "^4.0.0" semver "^6.0.0" +ts-loader@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.2.tgz#dffa3879b01a1a1e0a4b85e2b8421dc0dfff1c58" + integrity sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + tslib@^1, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -11801,11 +11826,16 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.3.3, typescript@^3.4.4: +typescript@^3.3.3: version "3.8.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a" integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ== +typescript@^3.4.4: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + ua-parser-js@^0.7.18: version "0.7.20" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.20.tgz#7527178b82f6a62a0f243d1f94fd30e3e3c21098"