From c9327375b19a1540a424f1f9bce57d7ae957ac0e Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Thu, 28 May 2020 14:03:14 +0300 Subject: [PATCH] [Calendar] Remove promise based loading in favor of `loading` prop (#1829) * Remove `onMonthChange` promise-based api in favor of `loading` prop * Update server request example for new API * Implement fake api for server request example * Update tests * Make CalendarSkeleton component and use it in the examples for loading state --- .gitignore | 2 + docs/.gitignore | 1 + docs/babel.config.js | 3 + docs/fakeApi/randomDate.ts | 32 ++++ docs/package.json | 3 + .../demo/datepicker/ServerRequest.example.jsx | 56 +++++-- docs/pages/demo/datepicker/index.mdx | 5 +- docs/prop-types.json | 154 +++++++++++------- lib/package.json | 4 + lib/src/CalendarSkeleton.tsx | 66 ++++++++ lib/src/__tests__/DatePicker.test.tsx | 81 +++------ lib/src/typings/overrides.ts | 1 + lib/src/typings/props.ts | 2 + lib/src/views/Calendar/Calendar.tsx | 137 ++++++++-------- lib/src/views/Calendar/CalendarView.tsx | 57 +++---- lib/src/views/Calendar/useCalendarState.tsx | 43 +---- now.json | 12 +- yarn.lock | 26 ++- 18 files changed, 416 insertions(+), 269 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/fakeApi/randomDate.ts create mode 100644 lib/src/CalendarSkeleton.tsx diff --git a/.gitignore b/.gitignore index e8bf63b2c..c5a14a4d2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ coverage # editors .vs .DS_Store + +.vercel \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..58d8196f8 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +.vercel \ No newline at end of file diff --git a/docs/babel.config.js b/docs/babel.config.js index 29a2ddfe4..744f4db8c 100644 --- a/docs/babel.config.js +++ b/docs/babel.config.js @@ -7,6 +7,9 @@ module.exports = { 'babel-plugin-module-resolver', { root: ['./'], + alias: { + '@material-ui/pickers/CalendarSkeleton': '@material-ui/pickers/src/CalendarSkeleton', + }, }, ], ], diff --git a/docs/fakeApi/randomDate.ts b/docs/fakeApi/randomDate.ts new file mode 100644 index 000000000..c305cbbdd --- /dev/null +++ b/docs/fakeApi/randomDate.ts @@ -0,0 +1,32 @@ +import { getDaysInMonth, isValid } from 'date-fns'; +import { NowRequest, NowResponse } from '@now/node'; + +function getRandomNumber(min: number, max: number) { + return Math.round(Math.random() * (max - min) + min); +} + +export default function(req: NowRequest, res: NowResponse) { + const { month } = req.query; + + if (!month || typeof month !== 'string') { + res.status(400); + return res.json({ + reason: 'month query param is required', + }); + } + + const date = new Date(month); + if (!isValid(date)) { + res.status(422); + return res.json({ + reason: 'cannot parse month value', + }); + } + + setTimeout(() => { + const daysInMonth = getDaysInMonth(date); + const daysToHighlight = [1, 2, 3].map(_ => getRandomNumber(1, daysInMonth)); + + res.json({ daysToHighlight }); + }, 500); // fake some long work +} diff --git a/docs/package.json b/docs/package.json index 36d97aea6..95852546a 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,7 +23,9 @@ "@mapbox/rehype-prism": "^0.4.0", "@material-ui/core": "^4.9.14", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.54", "@material-ui/pickers": "^4.0.0-alpha.1", + "@now/node": "^1.6.1", "@types/fuzzy-search": "^2.1.0", "@types/isomorphic-fetch": "^0.0.35", "@types/jss": "^10.0.0", @@ -57,6 +59,7 @@ "next-images": "^1.4.0", "next-transpile-modules": "^2.0.0", "notistack": "^0.9.11", + "now": "^19.0.1", "prismjs": "^1.20.0", "raw-loader": "^1.0.0", "react": "^16.13.0", diff --git a/docs/pages/demo/datepicker/ServerRequest.example.jsx b/docs/pages/demo/datepicker/ServerRequest.example.jsx index f7cebe89a..f1d97d726 100644 --- a/docs/pages/demo/datepicker/ServerRequest.example.jsx +++ b/docs/pages/demo/datepicker/ServerRequest.example.jsx @@ -1,41 +1,61 @@ -import React, { useState } from 'react'; +import * as React from 'react'; import { Badge } from '@material-ui/core'; import { TextField } from '@material-ui/core'; import { DatePicker, Day } from '@material-ui/pickers'; import { makeJSDateObject } from '../../../utils/helpers'; - -function getRandomNumber(min, max) { - return Math.round(Math.random() * (max - min) + min); -} +import { CalendarSkeleton } from '@material-ui/pickers/CalendarSkeleton'; function ServerRequest() { - const [selectedDays, setSelectedDays] = useState([1, 2, 15]); - const [selectedDate, handleDateChange] = useState(new Date()); - - const handleMonthChange = async () => { - // just select random days to simulate server side based data - return new Promise(resolve => { - setTimeout(() => { - setSelectedDays([1, 2, 3].map(() => getRandomNumber(1, 28))); - resolve(); - }, 1000); - }); + const requestAbortController = React.useRef(null); + const [highlightedDays, setHighlightedDays] = React.useState([1, 2, 15]); + const [selectedDate, handleDateChange] = React.useState(new Date()); + + React.useEffect(() => { + // abort request on unmount + return () => requestAbortController.current?.abort(); + }, []); + + const handleMonthChange = date => { + if (requestAbortController.current) { + // make sure that you are aborting useless requests + // because it is possible to switch between months pretty quickly + requestAbortController.current.abort(); + } + + setHighlightedDays(null); + + const controller = new AbortController(); + fetch(`/fakeApi/randomDate?month=${date.toString()}`, { + signal: controller.signal, + }) + .then(res => res.json()) + .then(({ daysToHighlight }) => setHighlightedDays(daysToHighlight)) + .catch(() => console.log('Wow, you are switching months too quickly 🐕')); + + requestAbortController.current = controller; }; return ( <> handleDateChange(date)} onMonthChange={handleMonthChange} + // loading renderInput={props => } + renderLoading={() => } renderDay={(day, selectedDate, DayComponentProps) => { const date = makeJSDateObject(day); // skip this step, it is required to support date libs const isSelected = - DayComponentProps.inCurrentMonth && selectedDays.includes(date.getDate()); + DayComponentProps.inCurrentMonth && highlightedDays.includes(date.getDate()); return ( - + ); diff --git a/docs/pages/demo/datepicker/index.mdx b/docs/pages/demo/datepicker/index.mdx index 46123afde..b626f9d5b 100644 --- a/docs/pages/demo/datepicker/index.mdx +++ b/docs/pages/demo/datepicker/index.mdx @@ -68,9 +68,10 @@ You can leverage our internal [Day](/api/Day) component, and render it in defaul #### Dynamic data -Sometimes it's required to display additional info right in the calendar. -For this just return `Promise` in `onMonthChange`. +Sometimes it's required to display additional info right in the calendar. Here is an example of prefetching +and displaying server side data using `onMonthChange`, `loading` and `renderDay` props. + #### API diff --git a/docs/prop-types.json b/docs/prop-types.json index 2d4cd5a12..7b0dbbfb0 100644 --- a/docs/prop-types.json +++ b/docs/prop-types.json @@ -525,6 +525,36 @@ "name": "boolean" } }, + "loading": { + "defaultValue": { + "value": "false" + }, + "description": "If `true` renders `LoadingComponent` in calendar instead of calendar view.\nCan be used to preload information and show it in calendar.", + "name": "loading", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "renderLoading": { + "defaultValue": { + "value": "() => \"...\"" + }, + "description": "Component displaying when passed `loading` true.", + "name": "renderLoading", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "ComponentClass<{}, any> | FunctionComponent<{}>" + } + }, "reduceAnimations": { "defaultValue": { "value": "/(android)/i.test(window.navigator.userAgent)." @@ -542,7 +572,7 @@ }, "onMonthChange": { "defaultValue": null, - "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved.", + "description": "Callback firing on month change.", "name": "onMonthChange", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", @@ -550,7 +580,7 @@ }, "required": false, "type": { - "name": "(date: DateIOType) => void | Promise" + "name": "(date: any) => void" } }, "renderDay": { @@ -581,19 +611,6 @@ "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" - } - }, "onYearChange": { "defaultValue": null, "description": "Callback firing on year change.", @@ -2302,6 +2319,36 @@ "name": "boolean" } }, + "loading": { + "defaultValue": { + "value": "false" + }, + "description": "If `true` renders `LoadingComponent` in calendar instead of calendar view.\nCan be used to preload information and show it in calendar.", + "name": "loading", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "renderLoading": { + "defaultValue": { + "value": "() => \"...\"" + }, + "description": "Component displaying when passed `loading` true.", + "name": "renderLoading", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "ComponentClass<{}, any> | FunctionComponent<{}>" + } + }, "reduceAnimations": { "defaultValue": { "value": "/(android)/i.test(window.navigator.userAgent)." @@ -2319,7 +2366,7 @@ }, "onMonthChange": { "defaultValue": null, - "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved.", + "description": "Callback firing on month change.", "name": "onMonthChange", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", @@ -2327,7 +2374,7 @@ }, "required": false, "type": { - "name": "(date: DateIOType) => void | Promise" + "name": "(date: any) => void" } }, "renderDay": { @@ -2343,19 +2390,6 @@ "name": "(day: DateIOType, selectedDates: DateIOType[], DayComponentProps: DayProps) => Element" } }, - "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" - } - }, "onYearChange": { "defaultValue": null, "description": "Callback firing on year change.", @@ -3160,6 +3194,36 @@ "name": "boolean" } }, + "loading": { + "defaultValue": { + "value": "false" + }, + "description": "If `true` renders `LoadingComponent` in calendar instead of calendar view.\nCan be used to preload information and show it in calendar.", + "name": "loading", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "boolean" + } + }, + "renderLoading": { + "defaultValue": { + "value": "() => \"...\"" + }, + "description": "Component displaying when passed `loading` true.", + "name": "renderLoading", + "parent": { + "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", + "name": "CalendarViewProps" + }, + "required": false, + "type": { + "name": "ComponentClass<{}, any> | FunctionComponent<{}>" + } + }, "reduceAnimations": { "defaultValue": { "value": "/(android)/i.test(window.navigator.userAgent)." @@ -3177,7 +3241,7 @@ }, "onMonthChange": { "defaultValue": null, - "description": "Callback firing on month change. Return promise to render spinner till it will not be resolved.", + "description": "Callback firing on month change.", "name": "onMonthChange", "parent": { "fileName": "material-ui-pickers/lib/src/views/Calendar/CalendarView.tsx", @@ -3185,7 +3249,7 @@ }, "required": false, "type": { - "name": "(date: DateIOType) => void | Promise" + "name": "(date: any) => void" } }, "renderDay": { @@ -3216,19 +3280,6 @@ "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" - } - }, "shouldDisableYear": { "defaultValue": null, "description": "Disable specific years dynamically.\nWorks like `shouldDisableDate` but for year selection view..", @@ -3878,19 +3929,6 @@ "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" - } - }, "disableHighlightToday": { "defaultValue": { "value": "false" diff --git a/lib/package.json b/lib/package.json index 7f15ad624..99f66105c 100644 --- a/lib/package.json +++ b/lib/package.json @@ -33,6 +33,7 @@ }, "peerDependencies": { "@material-ui/core": "^4.9.14", + "@material-ui/lab": "^4.0.0-alpha.54", "@types/react": "^16.8.6", "react": "^16.8.4", "react-dom": "^16.8.4" @@ -40,6 +41,9 @@ "peerDependenciesMeta": { "@types/react": { "optional": true + }, + "@material-ui/lab": { + "optional": true } }, "dependencies": { diff --git a/lib/src/CalendarSkeleton.tsx b/lib/src/CalendarSkeleton.tsx new file mode 100644 index 000000000..3ef6562e3 --- /dev/null +++ b/lib/src/CalendarSkeleton.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import Skeleton from '@material-ui/lab/Skeleton'; +import { makeStyles } from '@material-ui/core'; +import { DAY_SIZE, DAY_MARGIN } from './constants/dimensions'; +import { withDefaultProps } from './_shared/withDefaultProps'; +import { useStyles as useCalendarStyles } from './views/Calendar/Calendar'; + +export interface CalendarSkeletonProps extends React.HTMLProps {} + +const muiComponentConfig = { + name: 'MuiPickersCalendarSkeleton', +}; + +export const useStyles = makeStyles( + { + root: { + alignSelf: 'start', + }, + daySkeleton: { + margin: `0 ${DAY_MARGIN}px`, + }, + hidden: { + visibility: 'hidden', + }, + }, + muiComponentConfig +); + +const monthMap = [ + [0, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 0, 0, 0], +]; + +export const CalendarSkeleton: React.FC = withDefaultProps( + muiComponentConfig, + ({ className, ...other }) => { + const classes = useStyles(); + const calendarClasses = useCalendarStyles(); + + return ( +
+ {monthMap.map((week, i) => ( +
+ {week.map((day, i) => ( + + ))} +
+ ))} +
+ ); + } +); + +export default CalendarSkeleton; diff --git a/lib/src/__tests__/DatePicker.test.tsx b/lib/src/__tests__/DatePicker.test.tsx index 86abd333f..996c70442 100644 --- a/lib/src/__tests__/DatePicker.test.tsx +++ b/lib/src/__tests__/DatePicker.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import Picker from '../Picker/Picker'; +import CalendarSkeleton from '../CalendarSkeleton'; import { ReactWrapper } from 'enzyme'; import { TextField } from '@material-ui/core'; import { MaterialUiPickersDate } from '../typings/date'; @@ -94,7 +95,6 @@ describe('e2e - DatePicker inline variant', () => { onChange={onChangeMock} onClose={onCloseMock} onOpen={onOpenMock} - loadingIndicator={
} value={utilsToUse.date('2018-01-01T00:00:00.000Z')} /> ); @@ -131,34 +131,7 @@ describe('e2e - DatePicker inline variant', () => { }); }); -describe('e2e - DatePicker without month change', () => { - let component: ReactWrapper; - const onChangeMock = jest.fn(); - const date = utilsToUse.date('2018-01-01T00:00:00.000Z'); - - beforeEach(() => { - component = mount( - } - open - loadingIndicator={
} - onChange={onChangeMock} - value={date} - /> - ); - }); - - it('Should not add to loading queue if callback is undefined', () => { - component - .find('CalendarHeader button') - .first() - .simulate('click'); - - expect(component.find('[data-mui-test="loading"]').length).toEqual(0); - }); -}); - -describe('e2e - DatePicker month change sync', () => { +describe('e2e - DatePicker onMonthChange', () => { let component: ReactWrapper; const onChangeMock = jest.fn(); const onMonthChangeMock = jest.fn(); @@ -176,49 +149,46 @@ describe('e2e - DatePicker month change sync', () => { ); }); - it('Should not add to loading queue when synchronous', () => { + it('Should dispatch onMonthChange on month switches', () => { component .find('button[data-mui-test="previous-arrow-button"]') .first() .simulate('click'); - expect(component.find('[data-mui-test="loading-progress"]').length).toBe(0); + expect(onMonthChangeMock).toBeCalled(); }); }); -describe('e2e - DatePicker month change async', () => { - jest.useFakeTimers(); - let component: ReactWrapper; - const onChangeMock = jest.fn(); - - const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - const onMonthChangeAsyncMock = jest.fn(() => sleep(10)); - - const date = utilsToUse.date('2018-01-01T00:00:00.000Z'); - - beforeEach(() => { - component = mount( +describe('e2e - DatePicker loading prop', () => { + it('Should display loading indicator instead of calendar when `loading` passed', () => { + const component = mount( } open - onChange={onChangeMock} - onMonthChange={onMonthChangeAsyncMock} - value={date} + loading + renderInput={props => } + onChange={jest.fn()} + value={utilsToUse.date('2018-01-01T00:00:00.000Z')} /> ); - }); - it('Should add to loading queue when loading asynchronous data', () => { - component.find('button[data-mui-test="previous-arrow-button"]').simulate('click'); - - expect(component.find('[data-mui-test="loading-progress"]').length).toBeGreaterThan(1); + expect(component.find('[data-mui-test="day"]').length).toBe(0); + expect(component.find('[data-mui-test="loading-progress"]').length).toBe(1); }); - it.skip('Should empty loading queue after loading asynchronous data', async () => { - component.find('button[data-mui-test="previous-arrow-button"]').simulate('click'); - jest.runTimersToTime(10); + it('Should display custom LoadingComponent when `loading` passed', () => { + const component = mount( + } + onChange={jest.fn()} + renderLoading={() => } + value={utilsToUse.date('2018-01-01T00:00:00.000Z')} + /> + ); expect(component.find('[data-mui-test="loading-progress"]').length).toBe(0); + expect(component.find('div[data-mui-test="custom-loading"]').length).toBe(1); }); }); @@ -273,6 +243,7 @@ it('Should not add to loading queue when synchronous', () => { .find('button[data-mui-test="day"]') .at(0) .simulate('click'); + expect(component.find('h4[data-mui-test="datepicker-toolbar-date"]').text()).not.toBe( 'Enter Date' ); diff --git a/lib/src/typings/overrides.ts b/lib/src/typings/overrides.ts index aa1d4d0a6..df2d20581 100644 --- a/lib/src/typings/overrides.ts +++ b/lib/src/typings/overrides.ts @@ -64,4 +64,5 @@ export interface MuiPickersComponentsToClassName { typeof import('../DateRangePicker/DateRangePickerInput').useStyles >; MuiPickersModalDialog?: Classes; + MuiPickersCalendarSkeleton?: Classes; } diff --git a/lib/src/typings/props.ts b/lib/src/typings/props.ts index 5ff1c8d48..993497e2e 100644 --- a/lib/src/typings/props.ts +++ b/lib/src/typings/props.ts @@ -1,4 +1,5 @@ import { PickerProps } from '../Picker/Picker'; +import { CalendarSkeletonProps } from '../CalendarSkeleton'; import { DateRangeDelimiterProps } from '../DateRangePicker/DateRangeDelimiter'; import { ToolbarComponentProps, @@ -55,4 +56,5 @@ export interface MuiPickersComponentsPropsList { MuiPickersMobileDateRangePicker: MobileDateRangePickerProps; MuiPickersStaticDateRangePicker: StaticDateRangePickerProps; MuiPickersDateRangeDelimiter: DateRangeDelimiterProps; + MuiPickersCalendarSkeleton: CalendarSkeletonProps; } diff --git a/lib/src/views/Calendar/Calendar.tsx b/lib/src/views/Calendar/Calendar.tsx index 180eff8fc..42fb274e7 100644 --- a/lib/src/views/Calendar/Calendar.tsx +++ b/lib/src/views/Calendar/Calendar.tsx @@ -31,9 +31,16 @@ export interface ExportedCalendarProps */ allowKeyboardControl?: boolean; /** - * Custom loading indicator. + * If `true` renders `LoadingComponent` in calendar instead of calendar view. + * Can be used to preload information and show it in calendar. + * @default false */ - loadingIndicator?: JSX.Element; + loading?: boolean; + /** + * Component displaying when passed `loading` true. + * @default () => "..." + */ + renderLoading?: () => React.ReactNode; } export interface CalendarProps extends ExportedCalendarProps { @@ -51,20 +58,17 @@ export interface CalendarProps extends ExportedCalendarProps { } const muiComponentConfig = { name: 'MuiPickersCalendar' }; -export const useStyles = makeStyles( - theme => ({ - transitionContainer: { - minHeight: (DAY_SIZE + DAY_MARGIN * 4) * 6, +export const useStyles = makeStyles(theme => { + const weeksContainerHeight = (DAY_SIZE + DAY_MARGIN * 4) * 6; + return { + calendarContainer: { + minHeight: weeksContainerHeight, }, - transitionContainerOverflowAllowed: { - overflowX: 'visible', - }, - progressContainer: { - width: '100%', - height: '100%', + loadingContainer: { display: 'flex', justifyContent: 'center', alignItems: 'center', + minHeight: weeksContainerHeight, }, weekContainer: { overflow: 'hidden', @@ -96,9 +100,8 @@ export const useStyles = makeStyles( alignItems: 'center', color: theme.palette.text.hint, }, - }), - muiComponentConfig -); + }; +}, muiComponentConfig); export const Calendar: React.FC = withDefaultProps( muiComponentConfig, @@ -118,6 +121,8 @@ export const Calendar: React.FC = withDefaultProps( disableHighlightToday, showDaysOutsideCurrentMonth, className, + loading, + renderLoading = () => ..., TransitionProps, }) => { const now = useNow(); @@ -167,57 +172,61 @@ export const Calendar: React.FC = withDefaultProps( ))}
- -
- {utils.getWeekArray(currentMonth).map(week => ( -
- {week.map(day => { - const disabled = isDateDisabled(day); - const isDayInCurrentMonth = utils.getMonth(day) === currentMonthNumber; + {loading ? ( +
{renderLoading()}
+ ) : ( + +
+ {utils.getWeekArray(currentMonth).map(week => ( +
+ {week.map(day => { + const disabled = isDateDisabled(day); + const isDayInCurrentMonth = utils.getMonth(day) === currentMonthNumber; - const dayProps: DayProps = { - key: (day as any)?.toString(), - day: day, - role: 'cell', - isAnimating: isMonthSwitchingAnimating, - disabled: disabled, - allowKeyboardControl: allowKeyboardControl, - 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, - }; + const dayProps: DayProps = { + key: (day as any)?.toString(), + day: day, + role: 'cell', + isAnimating: isMonthSwitchingAnimating, + disabled: disabled, + allowKeyboardControl: allowKeyboardControl, + 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, + }; - return renderDay ? ( - renderDay(day, selectedDates, dayProps) - ) : ( - - ); - })} -
- ))} -
-
+ return renderDay ? ( + renderDay(day, selectedDates, dayProps) + ) : ( + + ); + })} +
+ ))} +
+
+ )} ); } diff --git a/lib/src/views/Calendar/CalendarView.tsx b/lib/src/views/Calendar/CalendarView.tsx index a318b213a..b27d79629 100644 --- a/lib/src/views/Calendar/CalendarView.tsx +++ b/lib/src/views/Calendar/CalendarView.tsx @@ -1,17 +1,15 @@ import * as React from 'react'; -import Grid from '@material-ui/core/Grid'; -import CircularProgress from '@material-ui/core/CircularProgress'; import { MonthSelection } from './MonthSelection'; import { DatePickerView } from '../../DatePicker'; import { useCalendarState } from './useCalendarState'; import { makeStyles } from '@material-ui/core/styles'; import { useUtils } from '../../_shared/hooks/useUtils'; -import { VIEW_HEIGHT } from '../../constants/dimensions'; import { MaterialUiPickersDate } from '../../typings/date'; import { FadeTransitionGroup } from './FadeTransitionGroup'; import { Calendar, ExportedCalendarProps } from './Calendar'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; import { withDefaultProps } from '../../_shared/withDefaultProps'; +import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; import { CalendarHeader, CalendarHeaderProps } from './CalendarHeader'; import { YearSelection, ExportedYearSelectionProps } from './YearSelection'; import { defaultMinDate, defaultMaxDate } from '../../constants/prop-types'; @@ -45,9 +43,9 @@ export interface CalendarViewProps */ reduceAnimations?: boolean; /** - * Callback firing on month change. Return promise to render spinner till it will not be resolved @DateIOType. + * Callback firing on month change. */ - onMonthChange?: (date: MaterialUiPickersDate) => void | Promise; + onMonthChange?: (date: MaterialUiPickersDate) => void; } export type ExportedCalendarViewProps = Omit< @@ -62,9 +60,12 @@ export const useStyles = makeStyles( viewTransitionContainer: { overflowY: 'auto', }, - gridFullHeight: { + fullHeightContainer: { flex: 1, - minHeight: VIEW_HEIGHT - 60, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: (DAY_SIZE + DAY_MARGIN * 4) * 7, height: '100%', }, }, @@ -85,12 +86,13 @@ export const CalendarView: React.FC = withDefaultProps( minDate: __minDate, maxDate: __maxDate, reduceAnimations = defaultReduceAnimations, - loadingIndicator = , shouldDisableDate, allowKeyboardControl: __allowKeyboardControlProp, disablePast, disableFuture, shouldDisableYear, + loading, + renderLoading, ...other }) => { const utils = useUtils(); @@ -102,7 +104,6 @@ export const CalendarView: React.FC = withDefaultProps( const maxDate = __maxDate || utils.date(defaultMaxDate); const { - loadingQueue, calendarState, changeFocusedDay, changeMonth, @@ -190,29 +191,21 @@ export const CalendarView: React.FC = withDefaultProps( /> )} - {view === 'date' && - (loadingQueue > 0 ? ( - - {loadingIndicator} - - ) : ( - - ))} + {view === 'date' && ( + + )}
diff --git a/lib/src/views/Calendar/useCalendarState.tsx b/lib/src/views/Calendar/useCalendarState.tsx index d425018be..2317f46f5 100644 --- a/lib/src/views/Calendar/useCalendarState.tsx +++ b/lib/src/views/Calendar/useCalendarState.tsx @@ -7,7 +7,6 @@ import { MuiPickersAdapter, useUtils, useNow } from '../../_shared/hooks/useUtil interface CalendarState { isMonthSwitchingAnimating: boolean; - loadingQueue: number; currentMonth: MaterialUiPickersDate; focusedDay: MaterialUiPickersDate | null; slideDirection: SlideDirection; @@ -27,22 +26,11 @@ export const createCalendarStateReducer = ( ) => ( state: CalendarState, action: - | ReducerAction<'popLoadingQueue'> | ReducerAction<'finishMonthSwitchingAnimation'> | ReducerAction<'changeMonth', ChangeMonthPayload> - | ReducerAction<'changeMonthLoading', ChangeMonthPayload> | ReducerAction<'changeFocusedDay', { focusedDay: MaterialUiPickersDate }> ): CalendarState => { switch (action.type) { - case 'changeMonthLoading': { - return { - ...state, - loadingQueue: state.loadingQueue + 1, - slideDirection: action.direction, - currentMonth: action.newMonth, - isMonthSwitchingAnimating: !reduceAnimations, - }; - } case 'changeMonth': { return { ...state, @@ -51,12 +39,6 @@ export const createCalendarStateReducer = ( isMonthSwitchingAnimating: !reduceAnimations, }; } - case 'popLoadingQueue': { - return { - ...state, - loadingQueue: state.loadingQueue <= 0 ? 0 : state.loadingQueue - 1, - }; - } case 'finishMonthSwitchingAnimation': { return { ...state, @@ -113,9 +95,8 @@ export function useCalendarState({ createCalendarStateReducer(Boolean(reduceAnimations), disableSwitchToMonthOnDayFocus, utils) ).current; - const [{ loadingQueue, ...calendarState }, dispatch] = React.useReducer(reducerFn, { + const [calendarState, dispatch] = React.useReducer(reducerFn, { isMonthSwitchingAnimating: false, - loadingQueue: 0, focusedDay: date, currentMonth: utils.startOfMonth(dateForMonth), slideDirection: 'left', @@ -123,20 +104,13 @@ export function useCalendarState({ 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, - }); + dispatch({ + type: 'changeMonth', + ...payload, + }); + + if (onMonthChange) { + onMonthChange(payload.newMonth); } }, [onMonthChange] @@ -185,7 +159,6 @@ export function useCalendarState({ ); return { - loadingQueue, calendarState, changeMonth, changeFocusedDay, diff --git a/now.json b/now.json index 3b8496066..be777960b 100644 --- a/now.json +++ b/now.json @@ -5,8 +5,18 @@ "IS_NOW": "true" } }, - "builds": [{ "src": "docs/next.config.js", "use": "@now/next" }], + "builds": [ + { "src": "docs/next.config.js", "use": "@now/next" }, + { + "src": "docs/fakeApi/*", + "use": "@now/node" + } + ], "routes": [ + { + "src": "/fakeApi/(.*)$", + "dest": "/docs/fakeApi/$1.ts" + }, { "src": "/api/(?[^/]+)$", "dest": "docs/api/props?component=$name" }, { "src": "/(.*)", "dest": "docs/$1" } ] diff --git a/yarn.lock b/yarn.lock index 70c5505c7..f4b66208a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2379,6 +2379,17 @@ dependencies: "@babel/runtime" "^7.4.4" +"@material-ui/lab@^4.0.0-alpha.54": + version "4.0.0-alpha.54" + resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.54.tgz#f359fac05667549353e5e21e631ae22cb2c22996" + integrity sha512-BK/z+8xGPQoMtG6gWKyagCdYO1/2DzkBchvvXs2bbTVh3sbi/QQLIqWV6UA1KtMVydYVt22NwV3xltgPkaPKLg== + dependencies: + "@babel/runtime" "^7.4.4" + "@material-ui/utils" "^4.9.6" + clsx "^1.0.4" + prop-types "^15.7.2" + react-is "^16.8.0" + "@material-ui/styles@^4.9.14": version "4.9.14" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.14.tgz#0a9e93a2bf24e8daa0811411a6f3dabdafbe9a07" @@ -2478,6 +2489,13 @@ "@nodelib/fs.scandir" "2.1.2" fastq "^1.6.0" +"@now/node@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@now/node/-/node-1.6.1.tgz#d478bfbc98af05d3eae7b5b01b26d1a1c638819b" + integrity sha512-EdSdOS4HSJRexibxIbrKCLZZ1sc0+oaD0P7kvhf26CbXSNlrZP0Iycpb+fbO4Qc2EcmGR2am90DhoX548B5a7g== + dependencies: + "@types/node" "*" + "@oclif/color@^0.0.0": version "0.0.0" resolved "https://registry.yarnpkg.com/@oclif/color/-/color-0.0.0.tgz#54939bbd16d1387511bf1a48ccda1a417248e6a9" @@ -10236,10 +10254,10 @@ notistack@^0.9.11: hoist-non-react-statics "^3.3.0" react-is "^16.8.6" -now@^18.0.0: - version "18.0.0" - resolved "https://registry.yarnpkg.com/now/-/now-18.0.0.tgz#e6689205346e354f822b1f5feb2c0d7d570c172f" - integrity sha512-MVskFV3xH1hMkpTewPCb3p0SQ17hL7EI1Zb+Ij2wYiRV3X0a6G91GdE2vvFlhsK6rhRHKHZnDQkZ0AnkpnfzHA== +now@^19.0.1: + version "19.0.1" + resolved "https://registry.yarnpkg.com/now/-/now-19.0.1.tgz#a4575adfed17ea049d9207c145f3ce7f60c818cf" + integrity sha512-Q/dUlRBPzoy6lHw9P9qnzGmXw3aBU5ypQ1JjrKwoQJoEVhU5hCa9dJCEtIiSIG5cModcirU0xJPemg54dvWPnA== npm-bundled@^1.0.1: version "1.0.6"