From 6d9b428cb5930402f6aae12cacb58c8695ea8e49 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Tue, 4 Aug 2020 12:16:15 +0300 Subject: [PATCH] [typescript] Make all components generic for date type (#2045) --- .../demo/datepicker/CustomDay.example.tsx | 2 +- .../demo/datepicker/ServerRequest.example.tsx | 2 - docs/pages/regression/RegressionDay.tsx | 4 +- .../{DatePicker.tsx => DatePicker.ts} | 65 ++-- lib/src/DateRangePicker/DateRangePicker.tsx | 20 +- .../DateRangePicker/DateRangePickerDay.tsx | 10 +- .../DateRangePicker/DateRangePickerView.tsx | 108 +++---- .../DateRangePickerViewDesktop.tsx | 74 ++--- .../DateRangePickerViewMobile.tsx | 63 ++-- lib/src/DateTimePicker/DateTimePicker.tsx | 54 ++-- lib/src/DateTimePicker/date-time-utils.ts | 4 +- lib/src/Picker/Picker.tsx | 2 +- lib/src/Picker/SharedPickerProps.tsx | 7 +- lib/src/Picker/makePickerWithState.tsx | 11 +- lib/src/TimePicker/TimePicker.tsx | 32 +- lib/src/TimePicker/TimePickerToolbar.tsx | 10 +- .../__tests__/shallow/MonthSelection.test.tsx | 6 +- .../typescript/GenericValues.spec.tsx | 74 +++++ lib/src/_helpers/date-utils.ts | 42 +-- lib/src/_helpers/time-utils.ts | 18 +- lib/src/_shared/KeyboardDateInput.tsx | 2 +- lib/src/_shared/PickerToolbar.tsx | 2 +- lib/src/_shared/hooks/date-helpers-hooks.tsx | 13 +- lib/src/_shared/hooks/useUtils.ts | 10 +- lib/src/_shared/hooks/useViews.tsx | 6 +- .../icons/{Calendar.tsx => CalendarIcon.tsx} | 0 lib/src/_shared/withDefaultProps.tsx | 23 +- lib/src/constants/prop-types.ts | 4 +- lib/src/index.ts | 12 +- lib/src/typings/props.ts | 10 +- lib/src/views/Calendar/Calendar.tsx | 49 +-- lib/src/views/Calendar/CalendarHeader.tsx | 65 ++-- lib/src/views/Calendar/CalendarView.tsx | 291 +++++++++--------- lib/src/views/Calendar/Day.tsx | 24 +- lib/src/views/Calendar/MonthSelection.tsx | 42 ++- lib/src/views/Calendar/YearSelection.tsx | 31 +- lib/src/views/Calendar/useCalendarState.tsx | 46 +-- lib/src/views/Clock/Clock.tsx | 21 +- lib/src/views/Clock/ClockView.tsx | 50 +-- package.json | 1 + 40 files changed, 731 insertions(+), 579 deletions(-) rename lib/src/DatePicker/{DatePicker.tsx => DatePicker.ts} (56%) rename lib/src/_shared/icons/{Calendar.tsx => CalendarIcon.tsx} (100%) diff --git a/docs/pages/demo/datepicker/CustomDay.example.tsx b/docs/pages/demo/datepicker/CustomDay.example.tsx index a046c3fa3b1d1c..47bd1ea269efc1 100644 --- a/docs/pages/demo/datepicker/CustomDay.example.tsx +++ b/docs/pages/demo/datepicker/CustomDay.example.tsx @@ -38,7 +38,7 @@ export default function CustomDay(demoProps: any) { const renderWeekPickerDay = ( date: Date, _selectedDates: Date[], - DayComponentProps: PickersDayProps + DayComponentProps: PickersDayProps ) => { const dateClone = makeJSDateObject(date); const selectedDateClone = makeJSDateObject(selectedDate ?? new Date()); diff --git a/docs/pages/demo/datepicker/ServerRequest.example.tsx b/docs/pages/demo/datepicker/ServerRequest.example.tsx index 599bb5a41fb6ec..eb2a6ef78591b1 100644 --- a/docs/pages/demo/datepicker/ServerRequest.example.tsx +++ b/docs/pages/demo/datepicker/ServerRequest.example.tsx @@ -43,13 +43,11 @@ export default function ServerRequest() { value={value} loading={highlightedDays === null} onChange={(newValue) => setValue(newValue)} - // @ts-expect-error fix typings of components onMonthChange={handleMonthChange} // loading renderInput={(props) => } renderLoading={() => } renderDay={(day, value, DayComponentProps) => { - // @ts-expect-error fix typings of components const date = makeJSDateObject(day ?? new Date()); // skip this step, it is required to support date libs const isSelected = DayComponentProps.inCurrentMonth && highlightedDays.includes(date.getDate()); diff --git a/docs/pages/regression/RegressionDay.tsx b/docs/pages/regression/RegressionDay.tsx index c7ea97f5a795e0..f61d6e41486298 100644 --- a/docs/pages/regression/RegressionDay.tsx +++ b/docs/pages/regression/RegressionDay.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { IUtils } from '@date-io/core/IUtils'; import { PickersDay, PickersDayProps } from '@material-ui/pickers'; -export const createRegressionDay = (utils: IUtils) => ( +export const createRegressionDay = (utils: IUtils) => ( day: any, selectedDate: any, - dayProps: PickersDayProps + dayProps: PickersDayProps ) => { return ; }; diff --git a/lib/src/DatePicker/DatePicker.tsx b/lib/src/DatePicker/DatePicker.ts similarity index 56% rename from lib/src/DatePicker/DatePicker.tsx rename to lib/src/DatePicker/DatePicker.ts index 452c167797fc00..b3f49e1e81b180 100644 --- a/lib/src/DatePicker/DatePicker.tsx +++ b/lib/src/DatePicker/DatePicker.ts @@ -2,25 +2,31 @@ import { useUtils } from '../_shared/hooks/useUtils'; import { DatePickerToolbar } from './DatePickerToolbar'; import { WithViewsProps } from '../Picker/SharedPickerProps'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; -import { useParsedDate } from '../_shared/hooks/date-helpers-hooks'; +import { useParsedDate, OverrideParsableDateProps } from '../_shared/hooks/date-helpers-hooks'; import { ExportedCalendarViewProps } from '../views/Calendar/CalendarView'; -import { MobileWrapper, DesktopWrapper, StaticWrapper } from '../wrappers/Wrapper'; +import { MobileWrapper, DesktopWrapper, StaticWrapper, SomeWrapper } from '../wrappers/Wrapper'; import { makeValidationHook, ValidationProps } from '../_shared/hooks/useValidation'; import { ParsableDate, defaultMinDate, defaultMaxDate } from '../constants/prop-types'; -import { makePickerWithStateAndWrapper, AllPickerProps } from '../Picker/makePickerWithState'; -import { getFormatAndMaskByViews, validateDate, DateValidationError } from '../_helpers/date-utils'; +import { + makePickerWithStateAndWrapper, + AllPickerProps, + SharedPickerProps, +} from '../Picker/makePickerWithState'; +import { getFormatAndMaskByViews, DateValidationError, validateDate } from '../_helpers/date-utils'; export type DatePickerView = 'year' | 'date' | 'month'; -export interface BaseDatePickerProps +export interface BaseDatePickerProps extends WithViewsProps<'year' | 'date' | 'month'>, ValidationProps, - ExportedCalendarViewProps {} + OverrideParsableDateProps, 'minDate' | 'maxDate'> {} const datePickerConfig = { - useValidation: makeValidationHook( - validateDate - ), + useValidation: makeValidationHook< + DateValidationError, + ParsableDate, + BaseDatePickerProps + >(validateDate), DefaultToolbarComponent: DatePickerToolbar, useInterceptProps: ({ openTo = 'date', @@ -28,7 +34,7 @@ const datePickerConfig = { minDate: __minDate = defaultMinDate, maxDate: __maxDate = defaultMaxDate, ...other - }: AllPickerProps) => { + }: AllPickerProps>) => { const utils = useUtils(); const minDate = useParsedDate(__minDate); const maxDate = useParsedDate(__maxDate); @@ -44,21 +50,31 @@ const datePickerConfig = { }, }; -export const DatePicker = makePickerWithStateAndWrapper(ResponsiveWrapper, { - name: 'MuiDatePicker', - ...datePickerConfig, -}); +type DatePickerComponent = ( + props: BaseDatePickerProps & SharedPickerProps +) => JSX.Element; + +export const DatePicker = makePickerWithStateAndWrapper>( + ResponsiveWrapper, + { + name: 'MuiDatePicker', + ...datePickerConfig, + } +) as DatePickerComponent; export type DatePickerProps = React.ComponentProps; -export const MobileDatePicker = makePickerWithStateAndWrapper(MobileWrapper, { - name: 'MuiMobileDatePicker', - ...datePickerConfig, -}); +export const MobileDatePicker = makePickerWithStateAndWrapper>( + MobileWrapper, + { + name: 'MuiMobileDatePicker', + ...datePickerConfig, + } +) as DatePickerComponent; export type MobileDatePickerProps = React.ComponentProps; -export const DesktopDatePicker = makePickerWithStateAndWrapper( +export const DesktopDatePicker = makePickerWithStateAndWrapper>( DesktopWrapper, { name: 'MuiDesktopDatePicker', @@ -68,9 +84,12 @@ export const DesktopDatePicker = makePickerWithStateAndWrapper; -export const StaticDatePicker = makePickerWithStateAndWrapper(StaticWrapper, { - name: 'MuiStaticDatePicker', - ...datePickerConfig, -}); +export const StaticDatePicker = makePickerWithStateAndWrapper>( + StaticWrapper, + { + name: 'MuiStaticDatePicker', + ...datePickerConfig, + } +) as DatePickerComponent; export type StaticDatePickerProps = React.ComponentProps; diff --git a/lib/src/DateRangePicker/DateRangePicker.tsx b/lib/src/DateRangePicker/DateRangePicker.tsx index 3b6f121b6bcd93..346097e83dc851 100644 --- a/lib/src/DateRangePicker/DateRangePicker.tsx +++ b/lib/src/DateRangePicker/DateRangePicker.tsx @@ -25,9 +25,9 @@ import { DateRangeValidationError, } from '../_helpers/date-utils'; -export interface BaseDateRangePickerProps - extends ExportedDateRangePickerViewProps, - ValidationProps, +export interface BaseDateRangePickerProps + extends ExportedDateRangePickerViewProps, + ValidationProps>, ExportedDateRangePickerInputProps { /** * Text for start input label and toolbar placeholder @@ -44,7 +44,7 @@ export interface BaseDateRangePickerProps } type RangePickerComponent = ( - props: BaseDateRangePickerProps & + props: BaseDateRangePickerProps & ExtendWrapper & AllSharedDateRangePickerProps & React.RefAttributes @@ -53,7 +53,7 @@ type RangePickerComponent = ( export const useDateRangeValidation = makeValidationHook< DateRangeValidationError, RangeInput, - BaseDateRangePickerProps + BaseDateRangePickerProps >(validateDateRange, { defaultValidationError: [null, null], isSameError: (a, b) => a[1] === b[1] && a[0] === b[0], @@ -82,10 +82,12 @@ export function makeRangePicker( startText = 'Start', endText = 'End', inputFormat: passedInputFormat, - minDate: __minDate = defaultMinDate, - maxDate: __maxDate = defaultMaxDate, + minDate: __minDate = defaultMinDate as TDate, + maxDate: __maxDate = defaultMaxDate as TDate, ...other - }: BaseDateRangePickerProps & AllSharedDateRangePickerProps & ExtendWrapper) { + }: BaseDateRangePickerProps & + AllSharedDateRangePickerProps & + ExtendWrapper) { const utils = useUtils(); const minDate = useParsedDate(__minDate); const maxDate = useParsedDate(__maxDate); @@ -126,7 +128,7 @@ export function makeRangePicker( return ( - open={wrapperProps.open} DateInputProps={DateInputProps} calendars={calendars} diff --git a/lib/src/DateRangePicker/DateRangePickerDay.tsx b/lib/src/DateRangePicker/DateRangePickerDay.tsx index 3a004630965efb..f3e3363acfed41 100644 --- a/lib/src/DateRangePicker/DateRangePickerDay.tsx +++ b/lib/src/DateRangePicker/DateRangePickerDay.tsx @@ -5,7 +5,7 @@ import { DAY_MARGIN } from '../constants/dimensions'; import { useUtils } from '../_shared/hooks/useUtils'; import { Day, DayProps, areDayPropsEqual } from '../views/Calendar/Day'; -export interface DateRangeDayProps extends DayProps { +export interface DateRangeDayProps extends DayProps { isHighlighting: boolean; isEndOfHighlighting: boolean; isStartOfHighlighting: boolean; @@ -96,7 +96,7 @@ const useStyles = makeStyles( { name: 'MuiPickersDateRangeDay' } ); -export const PureDateRangeDay = (props: DateRangeDayProps) => { +export function PureDateRangeDay(props: DateRangeDayProps) { const { className, day, @@ -136,7 +136,7 @@ export const PureDateRangeDay = (props: DateRangeDayProps) => { [classes.rangeIntervalDayPreviewStart]: isStartOfPreviewing || isStartOfMonth, })} > - {...other} disableMargin allowSameDateSelection @@ -158,7 +158,7 @@ export const PureDateRangeDay = (props: DateRangeDayProps) => { ); -}; +} PureDateRangeDay.displayName = 'DateRangeDay'; @@ -172,4 +172,4 @@ export const DateRangeDay = React.memo(PureDateRangeDay, (prevProps, nextProps) prevProps.isStartOfPreviewing === nextProps.isStartOfPreviewing && areDayPropsEqual(prevProps, nextProps) ); -}); +}) as typeof PureDateRangeDay; diff --git a/lib/src/DateRangePicker/DateRangePickerView.tsx b/lib/src/DateRangePicker/DateRangePickerView.tsx index ce7c196fe65ab2..83193362dffbd7 100644 --- a/lib/src/DateRangePicker/DateRangePickerView.tsx +++ b/lib/src/DateRangePicker/DateRangePickerView.tsx @@ -20,11 +20,14 @@ import { ExportedDesktopDateRangeCalendarProps, } from './DateRangePickerViewDesktop'; -type BaseCalendarPropsToReuse = Omit; - -export interface ExportedDateRangePickerViewProps - extends BaseCalendarPropsToReuse, - ExportedDesktopDateRangeCalendarProps, +type BaseCalendarPropsToReuse = Omit< + ExportedCalendarViewProps, + 'onYearChange' | 'renderDay' +>; + +export interface ExportedDateRangePickerViewProps + extends BaseCalendarPropsToReuse, + ExportedDesktopDateRangeCalendarProps, Omit { /** * if `true` after selecting `start` date calendar will not automatically switch to the month of `end` date @@ -34,47 +37,49 @@ export interface ExportedDateRangePickerViewProps disableAutoMonthSwitching?: boolean; } -interface DateRangePickerViewProps - extends ExportedDateRangePickerViewProps, - CurrentlySelectingRangeEndProps, +interface DateRangePickerViewProps + extends CurrentlySelectingRangeEndProps, + ExportedDateRangePickerViewProps, SharedPickerProps, DateRange, DateRangeInputProps> { open: boolean; startText: React.ReactNode; endText: React.ReactNode; } -export const DateRangePickerView = ({ - calendars = 2, - className, - currentlySelectingRangeEnd, - date, - DateInputProps, - disableAutoMonthSwitching = false, - disableFuture, - disableHighlightToday, - disablePast, - endText, - isMobileKeyboardViewOpen, - maxDate: unparsedMaxDate = defaultMaxDate, - minDate: unparsedMinDate = defaultMinDate, - onDateChange, - onMonthChange, - open, - reduceAnimations = defaultReduceAnimations, - setCurrentlySelectingRangeEnd, - shouldDisableDate, - showToolbar, - startText, - toggleMobileKeyboardView, - toolbarFormat, - toolbarTitle, - ...other -}: DateRangePickerViewProps) => { - const now = useNow(); - const utils = useUtils(); +export function DateRangePickerView(props: DateRangePickerViewProps) { + const { + calendars = 2, + className, + currentlySelectingRangeEnd, + date, + DateInputProps, + disableAutoMonthSwitching = false, + disableFuture, + disableHighlightToday, + disablePast, + endText, + isMobileKeyboardViewOpen, + maxDate: unparsedMaxDate = defaultMaxDate, + minDate: unparsedMinDate = defaultMinDate, + onDateChange, + onMonthChange, + open, + reduceAnimations = defaultReduceAnimations, + setCurrentlySelectingRangeEnd, + shouldDisableDate, + showToolbar, + startText, + toggleMobileKeyboardView, + toolbarFormat, + toolbarTitle, + ...other + } = props; + + const now = useNow(); + const utils = useUtils(); const wrapperVariant = React.useContext(WrapperVariantContext); - const minDate = useParsedDate(unparsedMinDate)!; - const maxDate = useParsedDate(unparsedMaxDate)!; + const minDate = useParsedDate(unparsedMinDate) as TDate; + const maxDate = useParsedDate(unparsedMaxDate) as TDate; const [start, end] = date; const { @@ -97,8 +102,14 @@ export const DateRangePickerView = ({ const toShowToolbar = showToolbar ?? wrapperVariant !== 'desktop'; - const scrollToDayIfNeeded = (day: unknown) => { - if (!utils.isValid(day) || isDateDisabled(day)) { + const scrollToDayIfNeeded = (day: TDate | null) => { + if (!day || !utils.isValid(day) || isDateDisabled(day)) { + return; + } + + const currentlySelectedDate = currentlySelectingRangeEnd === 'start' ? start : end; + if (currentlySelectedDate === null) { + // do not scroll if one of ages is not selected return; } @@ -113,9 +124,9 @@ export const DateRangePickerView = ({ ) { const newMonth = currentlySelectingRangeEnd === 'start' - ? start + ? currentlySelectedDate : // If need to focus end, scroll to the state when "end" is displaying in the last calendar - utils.addMonths(end, -displayingMonthRange); + utils.addMonths(currentlySelectedDate, -displayingMonthRange); changeMonth(newMonth); } @@ -126,18 +137,11 @@ export const DateRangePickerView = ({ 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: unknown) => { + (newDate: TDate | null) => { const { nextSelection, newRange } = calculateRangeChange({ newDate, utils, @@ -221,7 +225,7 @@ export const DateRangePickerView = ({ )} ); -}; +} DateRangePickerView.propTypes = { calendars: PropTypes.oneOf([1, 2, 3]), diff --git a/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx b/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx index f21fa6a9258a2c..d2a3a6e79d3b38 100644 --- a/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx +++ b/lib/src/DateRangePicker/DateRangePickerViewDesktop.tsx @@ -18,7 +18,7 @@ import { DateValidationProps, } from '../_helpers/date-utils'; -export interface ExportedDesktopDateRangeCalendarProps { +export interface ExportedDesktopDateRangeCalendarProps { /** * How many calendars render on **desktop** DateRangePicker. * @@ -29,16 +29,16 @@ export interface ExportedDesktopDateRangeCalendarProps { * Custom renderer for `` days. @DateIOType * @example (date, DateRangeDayProps) => */ - renderDay?: (date: unknown, DateRangeDayProps: DateRangeDayProps) => JSX.Element; + renderDay?: (date: TDate, DateRangeDayProps: DateRangeDayProps) => JSX.Element; } -interface DesktopDateRangeCalendarProps - extends ExportedDesktopDateRangeCalendarProps, - Omit, - DateValidationProps, +interface DesktopDateRangeCalendarProps + extends ExportedDesktopDateRangeCalendarProps, + Omit, 'renderDay'>, + DateValidationProps, ExportedArrowSwitcherProps { - date: DateRange; - changeMonth: (date: unknown) => void; + date: DateRange; + changeMonth: (date: TDate) => void; currentlySelectingRangeEnd: 'start' | 'end'; } @@ -67,7 +67,7 @@ export const useStyles = makeStyles( { name: 'MuiPickersDesktopDateRangeCalendar' } ); -function getCalendarsArray(calendars: ExportedDesktopDateRangeCalendarProps['calendars']) { +function getCalendarsArray(calendars: ExportedDesktopDateRangeCalendarProps['calendars']) { switch (calendars) { case 1: return [0]; @@ -81,32 +81,36 @@ function getCalendarsArray(calendars: ExportedDesktopDateRangeCalendarProps['cal } } -export const DateRangePickerViewDesktop: React.FC = ({ - date, - calendars = 2, - changeMonth, - leftArrowButtonProps, - leftArrowButtonText, - leftArrowIcon, - rightArrowButtonProps, - rightArrowButtonText, - rightArrowIcon, - onChange, - disableFuture, - disablePast, - minDate: __minDate, - maxDate: __maxDate, - currentlySelectingRangeEnd, - currentMonth, - renderDay = (_, props) => , - ...other -}) => { - const utils = useUtils(); +export function DateRangePickerViewDesktop(props: DesktopDateRangeCalendarProps) { + const { + date, + calendars = 2, + changeMonth, + leftArrowButtonProps, + leftArrowButtonText, + leftArrowIcon, + rightArrowButtonProps, + rightArrowButtonText, + rightArrowIcon, + onChange, + disableFuture, + disablePast, + // eslint-disable-next-line @typescript-eslint/naming-convention + minDate: __minDate, + // eslint-disable-next-line @typescript-eslint/naming-convention + maxDate: __maxDate, + currentlySelectingRangeEnd, + currentMonth, + renderDay = (_, dateRangeProps) => , + ...other + } = props; + + const utils = useUtils(); const classes = useStyles(); const minDate = __minDate || utils.date(defaultMinDate); const maxDate = __maxDate || utils.date(defaultMaxDate); - const [rangePreviewDay, setRangePreviewDay] = React.useState(null); + const [rangePreviewDay, setRangePreviewDay] = React.useState(null); const isNextMonthDisabled = useNextMonthDisabled(currentMonth, { disableFuture, maxDate }); const isPreviousMonthDisabled = usePreviousMonthDisabled(currentMonth, { disablePast, minDate }); @@ -119,14 +123,14 @@ export const DateRangePickerViewDesktop: React.FC }); const handleDayChange = React.useCallback( - (day: unknown) => { + (day: TDate | null) => { setRangePreviewDay(null); onChange(day); }, [onChange] ); - const handlePreviewDayChange = (newPreviewRequest: unknown) => { + const handlePreviewDayChange = (newPreviewRequest: TDate) => { if (!isWithinRange(utils, newPreviewRequest, date)) { setRangePreviewDay(newPreviewRequest); } else { @@ -172,7 +176,7 @@ export const DateRangePickerViewDesktop: React.FC rightArrowIcon={rightArrowIcon} text={utils.format(monthOnIteration, 'monthAndYear')} /> - {...other} key={index} date={date} @@ -198,4 +202,4 @@ export const DateRangePickerViewDesktop: React.FC })} ); -}; +} diff --git a/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx b/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx index ac97882062d6ac..9f48ae359bb36c 100644 --- a/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx +++ b/lib/src/DateRangePicker/DateRangePickerViewMobile.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; -import { CalendarHeader } from '../views/Calendar/CalendarHeader'; +import { CalendarHeader, ExportedCalendarHeaderProps } from '../views/Calendar/CalendarHeader'; import { DateRange } from './RangeTypes'; import { DateRangeDay } from './DateRangePickerDay'; import { useUtils } from '../_shared/hooks/useUtils'; import { Calendar, CalendarProps } from '../views/Calendar/Calendar'; -import { ExportedArrowSwitcherProps } from '../_shared/ArrowSwitcher'; import { defaultMinDate, defaultMaxDate } from '../constants/prop-types'; import { ExportedDesktopDateRangeCalendarProps } from './DateRangePickerViewDesktop'; import { @@ -14,35 +13,39 @@ import { DateValidationProps, } from '../_helpers/date-utils'; -export interface ExportedMobileDateRangeCalendarProps - extends Pick {} +export interface ExportedMobileDateRangeCalendarProps + extends Pick, 'renderDay'> {} -interface DesktopDateRangeCalendarProps - extends ExportedMobileDateRangeCalendarProps, - Omit, - DateValidationProps, - ExportedArrowSwitcherProps { - date: DateRange; - changeMonth: (date: unknown) => void; +interface DesktopDateRangeCalendarProps + extends ExportedMobileDateRangeCalendarProps, + Omit, 'date' | 'renderDay'>, + DateValidationProps, + ExportedCalendarHeaderProps { + date: DateRange; + changeMonth: (date: TDate) => void; } const onlyDateView = ['date'] as ['date']; -export const DateRangePickerViewMobile: React.FC = ({ - changeMonth, - date, - leftArrowButtonProps, - leftArrowButtonText, - leftArrowIcon, - maxDate: __maxDate, - minDate: __minDate, - onChange, - rightArrowButtonProps, - rightArrowButtonText, - rightArrowIcon, - renderDay = (_, props) => , - ...other -}) => { +export function DateRangePickerViewMobile(props: DesktopDateRangeCalendarProps) { + const { + changeMonth, + date, + leftArrowButtonProps, + leftArrowButtonText, + leftArrowIcon, + // eslint-disable-next-line @typescript-eslint/naming-convention + minDate: __minDate, + // eslint-disable-next-line @typescript-eslint/naming-convention + maxDate: __maxDate, + onChange, + rightArrowButtonProps, + rightArrowButtonText, + rightArrowIcon, + renderDay = (_, props) => {...props} />, + ...other + } = props; + const utils = useUtils(); const minDate = __minDate || utils.date(defaultMinDate); const maxDate = __maxDate || utils.date(defaultMaxDate); @@ -53,9 +56,9 @@ export const DateRangePickerViewMobile: React.FC view="date" views={onlyDateView} changeView={() => ({})} - onMonthChange={changeMonth} - leftArrowButtonProps={leftArrowButtonProps} + onMonthChange={changeMonth as any} leftArrowButtonText={leftArrowButtonText} + leftArrowButtonProps={leftArrowButtonProps} leftArrowIcon={leftArrowIcon} rightArrowButtonProps={rightArrowButtonProps} rightArrowButtonText={rightArrowButtonText} @@ -64,7 +67,7 @@ export const DateRangePickerViewMobile: React.FC maxDate={maxDate} {...other} /> - {...other} date={date} onChange={onChange} @@ -82,4 +85,4 @@ export const DateRangePickerViewMobile: React.FC /> ); -}; +} diff --git a/lib/src/DateTimePicker/DateTimePicker.tsx b/lib/src/DateTimePicker/DateTimePicker.tsx index 8d5351e6c2f99c..c9dd72f982bd33 100644 --- a/lib/src/DateTimePicker/DateTimePicker.tsx +++ b/lib/src/DateTimePicker/DateTimePicker.tsx @@ -3,10 +3,10 @@ import { DateTimePickerToolbar } from './DateTimePickerToolbar'; import { ExportedClockViewProps } from '../views/Clock/ClockView'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; import { pick12hOr24hFormat } from '../_helpers/text-field-helper'; -import { useParsedDate } from '../_shared/hooks/date-helpers-hooks'; +import { useParsedDate, OverrideParsableDateProps } from '../_shared/hooks/date-helpers-hooks'; import { ExportedCalendarViewProps } from '../views/Calendar/CalendarView'; -import { makePickerWithStateAndWrapper } from '../Picker/makePickerWithState'; -import { DesktopWrapper, MobileWrapper, StaticWrapper } from '../wrappers/Wrapper'; +import { makePickerWithStateAndWrapper, SharedPickerProps } from '../Picker/makePickerWithState'; +import { DesktopWrapper, MobileWrapper, StaticWrapper, SomeWrapper } from '../wrappers/Wrapper'; import { WithViewsProps, AllSharedPickerProps } from '../Picker/SharedPickerProps'; import { DateAndTimeValidationError, validateDateAndTime } from './date-time-utils'; import { makeValidationHook, ValidationProps } from '../_shared/hooks/useValidation'; @@ -14,11 +14,16 @@ import { ParsableDate, defaultMinDate, defaultMaxDate } from '../constants/prop- export type DateTimePickerView = 'year' | 'date' | 'month' | 'hours' | 'minutes' | 'seconds'; -export interface BaseDateTimePickerProps +type DateTimePickerViewsProps = OverrideParsableDateProps< + TDate, + ExportedClockViewProps & ExportedCalendarViewProps, + 'minDate' | 'maxDate' | 'minTime' | 'maxTime' +>; + +export interface BaseDateTimePickerProps extends WithViewsProps<'year' | 'date' | 'month' | 'hours' | 'minutes'>, ValidationProps, - ExportedClockViewProps, - ExportedCalendarViewProps { + DateTimePickerViewsProps { /** * To show tabs. */ @@ -34,11 +39,11 @@ export interface BaseDateTimePickerProps /** * Minimal selectable moment of time with binding to date, to set min time in each day use `minTime`. */ - minDateTime?: ParsableDate; + minDateTime?: ParsableDate; /** * Minimal selectable moment of time with binding to date, to set max time in each day use `maxTime`. */ - maxDateTime?: ParsableDate; + maxDateTime?: ParsableDate; /** * Date format, that is displaying in toolbar. */ @@ -58,7 +63,7 @@ function useInterceptProps({ orientation = 'portrait', views = ['year', 'date', 'hours', 'minutes'], ...other -}: BaseDateTimePickerProps & AllSharedPickerProps) { +}: BaseDateTimePickerProps & AllSharedPickerProps) { const utils = useUtils(); const minTime = useParsedDate(__minTime); const maxTime = useParsedDate(__maxTime); @@ -101,7 +106,7 @@ function useInterceptProps({ const useValidation = makeValidationHook< DateAndTimeValidationError, ParsableDate, - BaseDateTimePickerProps + BaseDateTimePickerProps >(validateDateAndTime); const dateTimePickerConfig = { @@ -110,42 +115,45 @@ const dateTimePickerConfig = { DefaultToolbarComponent: DateTimePickerToolbar, }; -export const DateTimePicker = makePickerWithStateAndWrapper( +type DateTimePickerComponent = ( + props: BaseDateTimePickerProps & SharedPickerProps +) => JSX.Element; + +export const DateTimePicker = makePickerWithStateAndWrapper>( ResponsiveWrapper, { name: 'MuiDateTimePicker', ...dateTimePickerConfig, } -); +) as DateTimePickerComponent; export type DateTimePickerProps = React.ComponentProps; -export const DesktopDateTimePicker = makePickerWithStateAndWrapper( - DesktopWrapper, - { - name: 'MuiDesktopDateTimePicker', - ...dateTimePickerConfig, - } -); +export const DesktopDateTimePicker = makePickerWithStateAndWrapper< + BaseDateTimePickerProps +>(DesktopWrapper, { + name: 'MuiDesktopDateTimePicker', + ...dateTimePickerConfig, +}) as DateTimePickerComponent; export type DesktopDateTimePickerProps = React.ComponentProps; -export const MobileDateTimePicker = makePickerWithStateAndWrapper( +export const MobileDateTimePicker = makePickerWithStateAndWrapper>( MobileWrapper, { name: 'MuiMobileDateTimePicker', ...dateTimePickerConfig, } -); +) as DateTimePickerComponent; export type MobileDateTimePickerProps = React.ComponentProps; -export const StaticDateTimePicker = makePickerWithStateAndWrapper( +export const StaticDateTimePicker = makePickerWithStateAndWrapper>( StaticWrapper, { name: 'MuiStaticDateTimePicker', ...dateTimePickerConfig, } -); +) as DateTimePickerComponent; export type StaticDateTimePickerProps = React.ComponentProps; diff --git a/lib/src/DateTimePicker/date-time-utils.ts b/lib/src/DateTimePicker/date-time-utils.ts index 365c3770d48dab..1a6f851f7b3655 100644 --- a/lib/src/DateTimePicker/date-time-utils.ts +++ b/lib/src/DateTimePicker/date-time-utils.ts @@ -3,7 +3,7 @@ import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; import { DateValidationProps, validateDate } from '../_helpers/date-utils'; import { TimeValidationProps, validateTime } from '../_helpers/time-utils'; -export function validateDateAndTime( +export function validateDateAndTime( utils: MuiPickersAdapter, value: unknown | ParsableDate, { @@ -13,7 +13,7 @@ export function validateDateAndTime( shouldDisableDate, disablePast, ...timeValidationProps - }: DateValidationProps & TimeValidationProps + }: DateValidationProps & TimeValidationProps ) { const dateValidationResult = validateDate(utils, value, { minDate, diff --git a/lib/src/Picker/Picker.tsx b/lib/src/Picker/Picker.tsx index c72c61be1d22ff..6d4161f95be71b 100644 --- a/lib/src/Picker/Picker.tsx +++ b/lib/src/Picker/Picker.tsx @@ -23,7 +23,7 @@ import { export interface ExportedPickerProps extends Omit, - CalendarAndClockProps, + CalendarAndClockProps, WithViewsProps { // TODO move out, cause it is DateTimePickerOnly hideTabs?: boolean; diff --git a/lib/src/Picker/SharedPickerProps.tsx b/lib/src/Picker/SharedPickerProps.tsx index 0fc4f88956bd12..5b0dfd14d57e63 100644 --- a/lib/src/Picker/SharedPickerProps.tsx +++ b/lib/src/Picker/SharedPickerProps.tsx @@ -44,12 +44,13 @@ export interface WithViewsProps { openTo?: T; } -export type CalendarAndClockProps = ExportedCalendarViewProps & ExportedClockViewProps; +export type CalendarAndClockProps = ExportedCalendarViewProps & + ExportedClockViewProps; export type ToolbarComponentProps< TDate = unknown, TView extends AnyPickerView = AnyPickerView -> = CalendarAndClockProps & { +> = CalendarAndClockProps & { ampmInClock?: boolean; date: TDate; dateRangeIcon?: React.ReactNode; @@ -58,7 +59,7 @@ export type ToolbarComponentProps< hideTabs?: boolean; isLandscape: boolean; isMobileKeyboardViewOpen: boolean; - onChange: PickerOnChangeFn; + onChange: PickerOnChangeFn; openView: TView; setOpenView: (view: TView) => void; timeIcon?: React.ReactNode; diff --git a/lib/src/Picker/makePickerWithState.tsx b/lib/src/Picker/makePickerWithState.tsx index a86b692d5d4327..88272f3bb3cbc2 100644 --- a/lib/src/Picker/makePickerWithState.tsx +++ b/lib/src/Picker/makePickerWithState.tsx @@ -38,15 +38,14 @@ const valueManager: PickerStateValueManager = { areValuesEqual: (utils: MuiPickersAdapter, a: unknown, b: unknown) => utils.isEqual(a, b), }; +export type SharedPickerProps = ExtendWrapper & + AllSharedPickerProps, TDate | null> & + React.RefAttributes; + type PickerComponent< TViewProps extends AllAvailableForOverrideProps, TWrapper extends SomeWrapper -> = ( - props: TViewProps & - ExtendWrapper & - AllSharedPickerProps, TDate | null> & - React.RefAttributes -) => JSX.Element; +> = (props: TViewProps & SharedPickerProps) => JSX.Element; export function makePickerWithStateAndWrapper< T extends AllAvailableForOverrideProps, diff --git a/lib/src/TimePicker/TimePicker.tsx b/lib/src/TimePicker/TimePicker.tsx index b320bca186389a..e2edb01d0f0e5c 100644 --- a/lib/src/TimePicker/TimePicker.tsx +++ b/lib/src/TimePicker/TimePicker.tsx @@ -5,18 +5,18 @@ import { TimePickerToolbar } from './TimePickerToolbar'; import { ExportedClockViewProps } from '../views/Clock/ClockView'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; import { pick12hOr24hFormat } from '../_helpers/text-field-helper'; -import { useParsedDate } from '../_shared/hooks/date-helpers-hooks'; +import { useParsedDate, OverrideParsableDateProps } from '../_shared/hooks/date-helpers-hooks'; import { useUtils, MuiPickersAdapter } from '../_shared/hooks/useUtils'; import { validateTime, TimeValidationError } from '../_helpers/time-utils'; -import { makePickerWithStateAndWrapper } from '../Picker/makePickerWithState'; -import { MobileWrapper, DesktopWrapper, StaticWrapper } from '../wrappers/Wrapper'; import { WithViewsProps, AllSharedPickerProps } from '../Picker/SharedPickerProps'; import { ValidationProps, makeValidationHook } from '../_shared/hooks/useValidation'; +import { MobileWrapper, DesktopWrapper, StaticWrapper, SomeWrapper } from '../wrappers/Wrapper'; +import { SharedPickerProps, makePickerWithStateAndWrapper } from '../Picker/makePickerWithState'; -export interface BaseTimePickerProps - extends ExportedClockViewProps, - ValidationProps, - WithViewsProps<'hours' | 'minutes' | 'seconds'> {} +export interface BaseTimePickerProps + extends ValidationProps>, + WithViewsProps<'hours' | 'minutes' | 'seconds'>, + OverrideParsableDateProps, 'minTime' | 'maxTime'> {} export function getTextFieldAriaText(value: ParsableDate, utils: MuiPickersAdapter) { return value && utils.isValid(utils.date(value)) @@ -61,16 +61,22 @@ function useInterceptProps({ const timePickerConfig = { useInterceptProps, - useValidation: makeValidationHook( - validateTime - ), + useValidation: makeValidationHook< + TimeValidationError, + ParsableDate, + BaseTimePickerProps + >(validateTime), DefaultToolbarComponent: TimePickerToolbar, }; +type TimePickerComponent = ( + props: BaseTimePickerProps & SharedPickerProps +) => JSX.Element; + export const TimePicker = makePickerWithStateAndWrapper(ResponsiveWrapper, { name: 'MuiTimePicker', ...timePickerConfig, -}); +}) as TimePickerComponent; export type TimePickerProps = React.ComponentProps; @@ -80,14 +86,14 @@ export const DesktopTimePicker = makePickerWithStateAndWrapper; export type DesktopTimePickerProps = React.ComponentProps; export const MobileTimePicker = makePickerWithStateAndWrapper(MobileWrapper, { name: 'MuiMobileTimePicker', ...timePickerConfig, -}); +}) as TimePickerComponent; export type MobileTimePickerProps = React.ComponentProps; diff --git a/lib/src/TimePicker/TimePickerToolbar.tsx b/lib/src/TimePicker/TimePickerToolbar.tsx index 5c423266d24008..bc8999af83d27d 100644 --- a/lib/src/TimePicker/TimePickerToolbar.tsx +++ b/lib/src/TimePicker/TimePickerToolbar.tsx @@ -53,17 +53,17 @@ export const useStyles = makeStyles( muiComponentConfig ); -export function useMeridiemMode( - date: unknown, +export function useMeridiemMode( + date: TDate, ampm: boolean | undefined, - onChange: PickerOnChangeFn + onChange: PickerOnChangeFn ) { - const utils = useUtils(); + const utils = useUtils(); const meridiemMode = getMeridiem(date, utils); const handleMeridiemChange = React.useCallback( (mode: 'am' | 'pm') => { - const timeWithMeridiem = convertToMeridiem(date, mode, Boolean(ampm), utils); + const timeWithMeridiem = convertToMeridiem(date, mode, Boolean(ampm), utils); onChange(timeWithMeridiem, 'partial'); }, [ampm, date, onChange, utils] diff --git a/lib/src/__tests__/shallow/MonthSelection.test.tsx b/lib/src/__tests__/shallow/MonthSelection.test.tsx index 0f0b5c0d31e04d..76c809ef11e98e 100644 --- a/lib/src/__tests__/shallow/MonthSelection.test.tsx +++ b/lib/src/__tests__/shallow/MonthSelection.test.tsx @@ -5,13 +5,13 @@ import { mount, utilsToUse } from '../test-utils'; import { MonthSelection, MonthSelectionProps } from '../../views/Calendar/MonthSelection'; describe('MonthSelection', () => { - let component: ReactWrapper; + let component: ReactWrapper>; beforeEach(() => { component = mount( diff --git a/lib/src/__tests__/typescript/GenericValues.spec.tsx b/lib/src/__tests__/typescript/GenericValues.spec.tsx index 344680a432fe4e..b7df49c3188ef8 100644 --- a/lib/src/__tests__/typescript/GenericValues.spec.tsx +++ b/lib/src/__tests__/typescript/GenericValues.spec.tsx @@ -5,7 +5,12 @@ import LuxonAdapter from '@material-ui/pickers/adapter/luxon'; import { DateTime } from 'luxon'; import { TextField } from '@material-ui/core'; import { DatePicker } from '@material-ui/pickers'; +import { TimePicker } from '../../TimePicker'; +import { ClockView } from '../../views/Clock/ClockView'; import { DateRangePicker } from '../../DateRangePicker/DateRangePicker'; +import { CalendarView } from '../../views/Calendar/CalendarView'; +import { Day } from '../../views/Calendar/Day'; +import { DateTimePicker } from '../../DateTimePicker'; // Allows to set date type right with generic JSX syntax @@ -18,6 +23,7 @@ import { DateRangePicker } from '../../DateRangePicker/DateRangePicker'; value={[new DateTime(), new DateTime()]} onChange={(date) => date[0]?.set({ second: 15 })} + renderDay={(day) => {day.toFormat('dd')} } renderInput={(props) => } />; @@ -68,6 +74,33 @@ const InferTest = () => { dateAdapter={new LuxonAdapter()} />; +// Allows inferring for side props + {day.toFormat('D')} } + onChange={(date) => date?.set({ second: 0 })} + renderInput={(props) => } +/>; + +// External components are generic as well + + view="date" + views={['date']} + date={moment()} + minDate={moment()} + maxDate={moment()} + onChange={(date) => date?.format()} + changeView={console.log} +/>; + + + day={new Date()} + allowSameDateSelection + inCurrentMonth + onDaySelect={(date) => date?.getDay()} +/>; + // Edge case and known issue. When the passed `value` is not a date type // We cannot infer the type properly without explicit generic type or `dateAdapter` prop // So in this case it is expected that type will be `null` as for now @@ -77,3 +110,44 @@ const InferTest = () => { onChange={(date) => date?.getDate()} renderInput={(props) => } />; + +/** ****************** */ +/* TimePicker */ +/** ****************** */ + + date?.set({ second: 0 })} + renderInput={(props) => } +/>; + +// Allows inferring for side props + date?.set({ second: 0 })} + renderInput={(props) => } +/>; + +// External components are generic as well + + type="hours" + date={null} + onChange={(date) => date?.getDate()} + // TODO cleanup public clock view props + nextViewAvailable + previousViewAvailable + openNextView={console.log} + openPreviousView={console.log} + onDateChange={console.log} +/>; + +/** ****************** */ +/* DateTimePicker */ +/** ****************** */ + + date?.set({ second: 0 })} + renderInput={(props) => } +/>; diff --git a/lib/src/_helpers/date-utils.ts b/lib/src/_helpers/date-utils.ts index 07cb001aee73b1..a1591352ab8b71 100644 --- a/lib/src/_helpers/date-utils.ts +++ b/lib/src/_helpers/date-utils.ts @@ -5,17 +5,17 @@ import { DatePickerView } from '../DatePicker/DatePicker'; import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; import { DateRange, RangeInput } from '../DateRangePicker/RangeTypes'; -interface FindClosestDateParams { - date: unknown; - utils: MuiPickersAdapter; - minDate: unknown; - maxDate: unknown; +interface FindClosestDateParams { + date: TDate; + utils: MuiPickersAdapter; + minDate: TDate; + maxDate: TDate; disableFuture: boolean; disablePast: boolean; - shouldDisableDate: (date: unknown) => boolean; + shouldDisableDate: (date: TDate) => boolean; } -export const findClosestEnabledDate = ({ +export const findClosestEnabledDate = ({ date, utils, minDate, @@ -23,8 +23,8 @@ export const findClosestEnabledDate = ({ disableFuture, disablePast, shouldDisableDate, -}: FindClosestDateParams) => { - const today = utils.startOfDay(utils.date()); +}: FindClosestDateParams) => { + const today = utils.startOfDay(utils.date()!); if (disablePast && utils.isBefore(minDate!, today)) { minDate = today; @@ -34,8 +34,8 @@ export const findClosestEnabledDate = ({ maxDate = today; } - let forward = date; - let backward = date; + let forward: TDate | null = date; + let backward: TDate | null = date; if (utils.isBefore(date, minDate)) { forward = utils.date(minDate); backward = null; @@ -143,19 +143,19 @@ export const isEndOfRange = (utils: MuiPickersAdapter, day: unknown, range: Date return isRangeValid(utils, range) && utils.isSameDay(day, range[1]); }; -export interface DateValidationProps { +export interface DateValidationProps { /** * Min selectable date. @DateIOType * * @default Date(1900-01-01) */ - minDate?: unknown; + minDate?: TDate; /** * Max selectable date. @DateIOType * * @default Date(2099-31-12) */ - maxDate?: unknown; + maxDate?: TDate; /** * Disable specific date. @DateIOType */ @@ -174,10 +174,10 @@ export interface DateValidationProps { disableFuture?: boolean; } -export const validateDate = ( +export const validateDate = ( utils: MuiPickersAdapter, - value: unknown | ParsableDate, - { minDate, maxDate, disableFuture, shouldDisableDate, disablePast }: DateValidationProps + value: TDate | ParsableDate, + { minDate, maxDate, disableFuture, shouldDisableDate, disablePast }: DateValidationProps ) => { const now = utils.date(); const date = utils.date(value); @@ -219,10 +219,10 @@ export type DateRangeValidationError = [ DateRangeValidationErrorValue ]; -export const validateDateRange = ( - utils: MuiPickersAdapter, - value: RangeInput, - dateValidationProps: DateValidationProps +export const validateDateRange = ( + utils: MuiPickersAdapter, + value: RangeInput, + dateValidationProps: DateValidationProps ): [DateRangeValidationErrorValue, DateRangeValidationErrorValue] => { const [start, end] = value; diff --git a/lib/src/_helpers/time-utils.ts b/lib/src/_helpers/time-utils.ts index 11c1e847d8e96a..a4ec7b33bb54e7 100644 --- a/lib/src/_helpers/time-utils.ts +++ b/lib/src/_helpers/time-utils.ts @@ -22,11 +22,11 @@ export const convertValueToMeridiem = (value: number, meridiem: Meridiem, ampm: return value; }; -export const convertToMeridiem = ( - time: unknown, +export const convertToMeridiem = ( + time: TDate, meridiem: 'am' | 'pm', ampm: boolean, - utils: MuiPickersAdapter + utils: MuiPickersAdapter ) => { const newHoursAmount = convertValueToMeridiem(utils.getHours(time), meridiem, ampm); return utils.setHours(time, newHoursAmount); @@ -103,17 +103,17 @@ export const createIsAfterIgnoreDatePart = ( return getSecondsInDay(dateLeft, utils) > getSecondsInDay(dateRight, utils); }; -export interface TimeValidationProps { +export interface TimeValidationProps { /** * Min time acceptable time. * For input validation date part of passed object will be ignored if `disableIgnoringDatePartForTimeValidation` not specified. */ - minTime?: unknown; + minTime?: TDate; /** * Max time acceptable time. * For input validation date part of passed object will be ignored if `disableIgnoringDatePartForTimeValidation` not specified. */ - maxTime?: unknown; + maxTime?: TDate; /** * Dynamically check if time is disabled or not. * If returns `false` appropriate time point will ot be acceptable. @@ -127,15 +127,15 @@ export interface TimeValidationProps { disableIgnoringDatePartForTimeValidation?: boolean; } -export const validateTime = ( +export const validateTime = ( utils: MuiPickersAdapter, - value: unknown | ParsableDate, + value: TDate | ParsableDate, { minTime, maxTime, shouldDisableTime, disableIgnoringDatePartForTimeValidation, - }: TimeValidationProps + }: TimeValidationProps ) => { const date = utils.date(value); const isAfterComparingFn = createIsAfterIgnoreDatePart( diff --git a/lib/src/_shared/KeyboardDateInput.tsx b/lib/src/_shared/KeyboardDateInput.tsx index 5f308f319298be..7855bc95594d6c 100644 --- a/lib/src/_shared/KeyboardDateInput.tsx +++ b/lib/src/_shared/KeyboardDateInput.tsx @@ -4,7 +4,7 @@ import IconButton from '@material-ui/core/IconButton'; import InputAdornment from '@material-ui/core/InputAdornment'; import { useForkRef } from '@material-ui/core/utils'; import { useUtils } from './hooks/useUtils'; -import { CalendarIcon } from './icons/Calendar'; +import { CalendarIcon } from './icons/CalendarIcon'; import { useMaskedInput } from './hooks/useMaskedInput'; import { DateInputProps, DateInputRefs } from './PureDateInput'; import { getTextFieldAriaText } from '../_helpers/text-field-helper'; diff --git a/lib/src/_shared/PickerToolbar.tsx b/lib/src/_shared/PickerToolbar.tsx index db02900780945e..306df4f3aba8ac 100644 --- a/lib/src/_shared/PickerToolbar.tsx +++ b/lib/src/_shared/PickerToolbar.tsx @@ -7,7 +7,7 @@ import { makeStyles } from '@material-ui/core/styles'; import Toolbar, { ToolbarProps } from '@material-ui/core/Toolbar'; import { ExtendMui } from '../typings/helpers'; import { PenIcon } from './icons/Pen'; -import { CalendarIcon } from './icons/Calendar'; +import { CalendarIcon } from './icons/CalendarIcon'; import { ToolbarComponentProps } from '../Picker/SharedPickerProps'; export const useStyles = makeStyles( diff --git a/lib/src/_shared/hooks/date-helpers-hooks.tsx b/lib/src/_shared/hooks/date-helpers-hooks.tsx index 4dc855cb9a752b..1990359bbbed0b 100644 --- a/lib/src/_shared/hooks/date-helpers-hooks.tsx +++ b/lib/src/_shared/hooks/date-helpers-hooks.tsx @@ -1,8 +1,17 @@ import * as React from 'react'; import { useUtils } from './useUtils'; +import { ParsableDate } from '../../constants/prop-types'; -export function useParsedDate(possiblyUnparsedValue: any): unknown | undefined { - const utils = useUtils(); +export type OverrideParsableDateProps = Omit< + TProps, + TKey +> & + Partial>>; + +export function useParsedDate( + possiblyUnparsedValue: ParsableDate +): TDate | undefined { + const utils = useUtils(); return React.useMemo( () => typeof possiblyUnparsedValue === 'undefined' ? undefined : utils.date(possiblyUnparsedValue)!, diff --git a/lib/src/_shared/hooks/useUtils.ts b/lib/src/_shared/hooks/useUtils.ts index 36b30b587e9ea5..00a7f660c16d7c 100644 --- a/lib/src/_shared/hooks/useUtils.ts +++ b/lib/src/_shared/hooks/useUtils.ts @@ -13,16 +13,16 @@ function checkUtils(utils: MuiPickersAdapter | null) /* :asserts utils is MuiPic } } -export function useUtils(): MuiPickersAdapter { +export function useUtils() { const utils = React.useContext(MuiPickersAdapterContext); checkUtils(utils); - return utils!; + return utils as MuiPickersAdapter; } -export function useNow() { - const utils = useUtils(); +export function useNow() { + const utils = useUtils(); const now = React.useRef(utils.date()); - return now.current; + return now.current!; } diff --git a/lib/src/_shared/hooks/useViews.tsx b/lib/src/_shared/hooks/useViews.tsx index f00198fb97e90a..a78cd2b96e2aff 100644 --- a/lib/src/_shared/hooks/useViews.tsx +++ b/lib/src/_shared/hooks/useViews.tsx @@ -3,8 +3,8 @@ import { arrayIncludes } from '../../_helpers/utils'; import { PickerSelectionState } from './usePickerState'; import { AnyPickerView } from '../../Picker/SharedPickerProps'; -export type PickerOnChangeFn = ( - date: TDate, +export type PickerOnChangeFn = ( + date: TDate | null, selectionState?: PickerSelectionState ) => void; @@ -17,7 +17,7 @@ export function useViews({ }: { views: AnyPickerView[]; openTo: AnyPickerView; - onChange: PickerOnChangeFn; + onChange: PickerOnChangeFn; isMobileKeyboardViewOpen: boolean; toggleMobileKeyboardView: () => void; }) { diff --git a/lib/src/_shared/icons/Calendar.tsx b/lib/src/_shared/icons/CalendarIcon.tsx similarity index 100% rename from lib/src/_shared/icons/Calendar.tsx rename to lib/src/_shared/icons/CalendarIcon.tsx diff --git a/lib/src/_shared/withDefaultProps.tsx b/lib/src/_shared/withDefaultProps.tsx index 650fc1deac82aa..67af9f37cad0c2 100644 --- a/lib/src/_shared/withDefaultProps.tsx +++ b/lib/src/_shared/withDefaultProps.tsx @@ -2,20 +2,25 @@ import * as React from 'react'; import getThemeProps from '@material-ui/styles/getThemeProps'; import { useTheme } from '@material-ui/core/styles'; +export function useDefaultProps(props: T, { name }: { name: string }) { + const theme = useTheme(); + + return getThemeProps({ + props, + theme, + name, + }); +} + export function withDefaultProps( - { name }: { name: string }, + componentConfig: { name: string }, Component: React.ComponentType ): React.FC { - const componentName = name.replace('Mui', ''); - const WithDefaultProps = (props: T) => { - const theme = useTheme(); - const propsWithDefault = getThemeProps({ - props, - theme, - name, - }); + const componentName = componentConfig.name.replace('Mui', ''); + const WithDefaultProps = (props: T) => { Component.displayName = componentName; + const propsWithDefault = useDefaultProps(props, componentConfig); return ; }; diff --git a/lib/src/constants/prop-types.ts b/lib/src/constants/prop-types.ts index 68a90f696dfbd8..3127993d3b2532 100644 --- a/lib/src/constants/prop-types.ts +++ b/lib/src/constants/prop-types.ts @@ -13,6 +13,6 @@ export type ParsableDate = string | number | Date | null | unde export const DomainPropTypes = { date, datePickerView }; -export const defaultMinDate = new Date('1900-01-01') as any; +export const defaultMinDate = new Date('1900-01-01') as unknown; -export const defaultMaxDate = new Date('2099-12-31') as any; +export const defaultMaxDate = new Date('2099-12-31') as unknown; diff --git a/lib/src/index.ts b/lib/src/index.ts index 2cfda50cd347cf..276f68f6c327ee 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -39,11 +39,13 @@ export { // TODO replace the following syntax with new ts export type { } syntax when will be supported by rollup -export type PickersCalendarProps = import('./views/Calendar/Calendar').CalendarProps; -export type PickersCalendarViewProps = import('./views/Calendar/CalendarView').CalendarViewProps; -export type PickersDayProps = import('./views/Calendar/Day').DayProps; -export type PickersClockViewProps = import('./views/Clock/ClockView').ClockViewProps; -export type PickersClockProps = import('./views/Clock/Clock').ClockProps; +export type PickersCalendarProps = import('./views/Calendar/Calendar').CalendarProps; +export type PickersCalendarViewProps< + TDate +> = import('./views/Calendar/CalendarView').CalendarViewProps; +export type PickersDayProps = import('./views/Calendar/Day').DayProps; +export type PickersClockViewProps = import('./views/Clock/ClockView').ClockViewProps; +export type PickersClockProps = import('./views/Clock/Clock').ClockProps; export type ToolbarComponentProps = import('./Picker/SharedPickerProps').ToolbarComponentProps; export type DateRangeDelimiterProps = import('./DateRangePicker/DateRangeDelimiter').DateRangeDelimiterProps; export type LocalizationProviderProps = import('./LocalizationProvider').LocalizationProviderProps; diff --git a/lib/src/typings/props.ts b/lib/src/typings/props.ts index 8fc989f7ac284c..21b69634611f8b 100644 --- a/lib/src/typings/props.ts +++ b/lib/src/typings/props.ts @@ -28,8 +28,8 @@ import { } from '..'; export interface MuiPickersComponentsPropsList { - MuiPickersDay: PickersDayProps; - MuiPickersCalendarView: PickersCalendarViewProps; + MuiPickersDay: PickersDayProps; + MuiPickersCalendarView: PickersCalendarViewProps; MuiPickersDatePicker: DatePickerProps; MuiPickersMobileDatePicker: MobileDatePickerProps; MuiPickersDesktopDatePicker: DesktopDatePickerProps; @@ -42,9 +42,9 @@ export interface MuiPickersComponentsPropsList { MuiPickersMobileDateTimePicker: MobileDateTimePickerProps; MuiPickersDesktopDateTimePicker: DesktopDateTimePickerProps; MuiPickersStaticDateTimePicker: StaticDateTimePickerProps; - MuiPickersCalendar: PickersCalendarProps; - MuiPickersClockView: PickersClockViewProps; - MuiPickersClock: PickersClockProps; + MuiPickersCalendar: PickersCalendarProps; + MuiPickersClockView: PickersClockViewProps; + MuiPickersClock: PickersClockProps; MuiPickersBasePicker: PickerProps; MuiPickersLocalizationProvider: LocalizationProviderProps; MuiPickersTimePickerToolbar: ToolbarComponentProps; diff --git a/lib/src/views/Calendar/Calendar.tsx b/lib/src/views/Calendar/Calendar.tsx index eb3bd06aef113e..e228df7478e571 100644 --- a/lib/src/views/Calendar/Calendar.tsx +++ b/lib/src/views/Calendar/Calendar.tsx @@ -6,24 +6,28 @@ import { Day, DayProps } from './Day'; import { useUtils, useNow } from '../../_shared/hooks/useUtils'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; -import { withDefaultProps } from '../../_shared/withDefaultProps'; +import { useDefaultProps } from '../../_shared/withDefaultProps'; import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; import { useGlobalKeyDown, keycode } from '../../_shared/hooks/useKeyDown'; import { SlideTransition, SlideDirection, SlideTransitionProps } from './SlideTransition'; -export interface ExportedCalendarProps +export interface ExportedCalendarProps extends Pick< - DayProps, + DayProps, 'disableHighlightToday' | 'showDaysOutsideCurrentMonth' | 'allowSameDateSelection' > { /** * Calendar onChange. */ - onChange: PickerOnChangeFn; + onChange: PickerOnChangeFn; /** * Custom renderer for day. Check [DayComponentProps api](https://material-ui-pickers.dev/api/Day) @DateIOType. */ - renderDay?: (day: unknown, selectedDates: unknown[], DayComponentProps: DayProps) => JSX.Element; + renderDay?: ( + day: TDate, + selectedDates: (TDate | null)[], + DayComponentProps: DayProps + ) => JSX.Element; /** * Enables keyboard listener for moving between days in calendar. * @@ -45,14 +49,14 @@ export interface ExportedCalendarProps renderLoading?: () => React.ReactNode; } -export interface CalendarProps extends ExportedCalendarProps { - date: unknown | unknown[]; - isDateDisabled: (day: unknown) => boolean; +export interface CalendarProps extends ExportedCalendarProps { + date: TDate | null | Array; + isDateDisabled: (day: TDate) => boolean; slideDirection: SlideDirection; - currentMonth: unknown; + currentMonth: TDate; reduceAnimations: boolean; - focusedDay: unknown | null; - changeFocusedDay: (newFocusedDay: unknown) => void; + focusedDay: TDate | null; + changeFocusedDay: (newFocusedDay: TDate) => void; isMonthSwitchingAnimating: boolean; onMonthSwitchingAnimationEnd: () => void; TransitionProps?: Partial; @@ -105,7 +109,7 @@ export const useStyles = makeStyles((theme) => { }; }, muiComponentConfig); -export const Calendar: React.FC = withDefaultProps(muiComponentConfig, (props) => { +export function Calendar(props: CalendarProps) { const { allowKeyboardControl, allowSameDateSelection, @@ -126,14 +130,15 @@ export const Calendar: React.FC = withDefaultProps(muiComponentCo showDaysOutsideCurrentMonth, slideDirection, TransitionProps, - } = props; - const now = useNow(); - const utils = useUtils(); + } = useDefaultProps(props, muiComponentConfig); + + const now = useNow(); + const utils = useUtils(); const theme = useTheme(); const classes = useStyles(); const handleDaySelect = React.useCallback( - (day: unknown, isFinish: PickerSelectionState = 'finish') => { + (day: TDate, isFinish: PickerSelectionState = 'finish') => { // TODO possibly buggy line figure out and add tests const finalDate = Array.isArray(date) ? day : utils.mergeDateAndTime(day, date || now); @@ -161,7 +166,7 @@ export const Calendar: React.FC = withDefaultProps(muiComponentCo const currentMonthNumber = utils.getMonth(currentMonth); const selectedDates = (Array.isArray(date) ? date : [date]) .filter(Boolean) - .map((selectedDateItem) => utils.startOfDay(selectedDateItem)); + .map((selectedDateItem) => selectedDateItem && utils.startOfDay(selectedDateItem)); return ( @@ -195,7 +200,7 @@ export const Calendar: React.FC = withDefaultProps(muiComponentCo const disabled = isDateDisabled(day); const isDayInCurrentMonth = utils.getMonth(day) === currentMonthNumber; - const dayProps: DayProps = { + const dayProps: DayProps = { key: (day as any)?.toString(), day, role: 'cell', @@ -206,11 +211,11 @@ export const Calendar: React.FC = withDefaultProps(muiComponentCo focused: allowKeyboardControl && Boolean(focusedDay) && - utils.isSameDay(day, focusedDay), + utils.isSameDay(day, nowFocusedDay), today: utils.isSameDay(day, now), inCurrentMonth: isDayInCurrentMonth, - selected: selectedDates.some((selectedDate) => - utils.isSameDay(selectedDate, day) + selected: selectedDates.some( + (selectedDate) => selectedDate && utils.isSameDay(selectedDate, day) ), disableHighlightToday, showDaysOutsideCurrentMonth, @@ -235,6 +240,6 @@ export const Calendar: React.FC = withDefaultProps(muiComponentCo )} ); -}); +} Calendar.displayName = 'Calendar'; diff --git a/lib/src/views/Calendar/CalendarHeader.tsx b/lib/src/views/Calendar/CalendarHeader.tsx index 86174f9cfdf037..d6cb89d5ebde62 100644 --- a/lib/src/views/Calendar/CalendarHeader.tsx +++ b/lib/src/views/Calendar/CalendarHeader.tsx @@ -17,21 +17,30 @@ import { useNextMonthDisabled, } from '../../_shared/hooks/date-helpers-hooks'; -export interface CalendarHeaderProps +export type ExportedCalendarHeaderProps = Pick< + CalendarHeaderProps, + | 'leftArrowIcon' + | 'rightArrowIcon' + | 'leftArrowButtonProps' + | 'rightArrowButtonProps' + | 'leftArrowButtonText' + | 'rightArrowButtonText' + | 'getViewSwitchingButtonText' +>; + +export interface CalendarHeaderProps extends ExportedArrowSwitcherProps, - Omit { + Omit, 'shouldDisableDate'> { view: DatePickerView; views: DatePickerView[]; - currentMonth: unknown; + currentMonth: TDate; /** * Get aria-label text for switching between views button. */ getViewSwitchingButtonText?: (currentView: DatePickerView) => string; reduceAnimations: boolean; changeView: (view: DatePickerView) => void; - minDate: unknown; - maxDate: unknown; - onMonthChange: (date: unknown, slideDirection: SlideDirection) => void; + onMonthChange: (date: TDate, slideDirection: SlideDirection) => void; } export const useStyles = makeStyles( @@ -81,26 +90,28 @@ function getSwitchingViewAriaText(view: DatePickerView) { : 'calendar view is open, switch to year view'; } -export const CalendarHeader: React.SFC = ({ - view: currentView, - views, - currentMonth: month, - changeView, - minDate, - maxDate, - disablePast, - disableFuture, - onMonthChange, - reduceAnimations, - leftArrowButtonProps, - rightArrowButtonProps, - leftArrowIcon, - rightArrowIcon, - leftArrowButtonText = 'previous month', - rightArrowButtonText = 'next month', - getViewSwitchingButtonText = getSwitchingViewAriaText, -}) => { - const utils = useUtils(); +export function CalendarHeader(props: CalendarHeaderProps) { + const { + view: currentView, + views, + currentMonth: month, + changeView, + minDate, + maxDate, + disablePast, + disableFuture, + onMonthChange, + reduceAnimations, + leftArrowButtonProps, + rightArrowButtonProps, + leftArrowIcon, + rightArrowIcon, + leftArrowButtonText = 'previous month', + rightArrowButtonText = 'next month', + getViewSwitchingButtonText = getSwitchingViewAriaText, + } = props; + + const utils = useUtils(); const classes = useStyles(); const selectNextMonth = () => onMonthChange(utils.getNextMonth(month), 'left'); @@ -187,7 +198,7 @@ export const CalendarHeader: React.SFC = ({ ); -}; +} CalendarHeader.displayName = 'PickersCalendarHeader'; diff --git a/lib/src/views/Calendar/CalendarView.tsx b/lib/src/views/Calendar/CalendarView.tsx index 383bf2ccecf409..f65eb0f31544c6 100644 --- a/lib/src/views/Calendar/CalendarView.tsx +++ b/lib/src/views/Calendar/CalendarView.tsx @@ -7,35 +7,24 @@ import { useUtils } from '../../_shared/hooks/useUtils'; import { FadeTransitionGroup } from './FadeTransitionGroup'; import { Calendar, ExportedCalendarProps } from './Calendar'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { withDefaultProps } from '../../_shared/withDefaultProps'; +import { useDefaultProps } from '../../_shared/withDefaultProps'; import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; -import { CalendarHeader, CalendarHeaderProps } from './CalendarHeader'; +import { CalendarHeader, ExportedCalendarHeaderProps } from './CalendarHeader'; import { YearSelection, ExportedYearSelectionProps } from './YearSelection'; import { defaultMinDate, defaultMaxDate } from '../../constants/prop-types'; import { IsStaticVariantContext } from '../../wrappers/WrapperVariantContext'; import { DateValidationProps, findClosestEnabledDate } from '../../_helpers/date-utils'; -type PublicCalendarHeaderProps = Pick< - CalendarHeaderProps, - | 'leftArrowIcon' - | 'rightArrowIcon' - | 'leftArrowButtonProps' - | 'rightArrowButtonProps' - | 'leftArrowButtonText' - | 'rightArrowButtonText' - | 'getViewSwitchingButtonText' ->; - -export interface CalendarViewProps - extends DateValidationProps, - ExportedCalendarProps, - ExportedYearSelectionProps, - PublicCalendarHeaderProps { - date: unknown; +export interface CalendarViewProps + extends DateValidationProps, + ExportedCalendarProps, + ExportedYearSelectionProps, + ExportedCalendarHeaderProps { + date: TDate; view: DatePickerView; views: DatePickerView[]; changeView: (view: DatePickerView) => void; - onChange: PickerOnChangeFn; + onChange: PickerOnChangeFn; /** * Disable heavy animations. * @@ -45,11 +34,11 @@ export interface CalendarViewProps /** * Callback firing on month change. @DateIOType */ - onMonthChange?: (date: unknown) => void; + onMonthChange?: (date: TDate) => void; } -export type ExportedCalendarViewProps = Omit< - CalendarViewProps, +export type ExportedCalendarViewProps = Omit< + CalendarViewProps, 'date' | 'view' | 'views' | 'onChange' | 'changeView' | 'slideDirection' | 'currentMonth' >; @@ -75,138 +64,136 @@ export const useStyles = makeStyles( export const defaultReduceAnimations = typeof navigator !== 'undefined' && /(android)/i.test(navigator.userAgent); -export const CalendarView: React.FC = withDefaultProps( - muiComponentConfig, - (props) => { - const { - allowKeyboardControl: allowKeyboardControlProp, - changeView, - date, - disableFuture, - disablePast, - loading, - maxDate: maxDateProp, - minDate: minDateProp, - onChange, - onMonthChange, - reduceAnimations = defaultReduceAnimations, - renderLoading, - shouldDisableDate, - shouldDisableYear, - view, - ...other - } = props; - const utils = useUtils(); - const classes = useStyles(); - const isStatic = React.useContext(IsStaticVariantContext); - const allowKeyboardControl = allowKeyboardControlProp ?? !isStatic; +export function CalendarView(props: CalendarViewProps) { + const { + allowKeyboardControl: allowKeyboardControlProp, + changeView, + date, + disableFuture, + disablePast, + loading, + maxDate: maxDateProp, + minDate: minDateProp, + onChange, + onMonthChange, + reduceAnimations = defaultReduceAnimations, + renderLoading, + shouldDisableDate, + shouldDisableYear, + view, + ...other + } = useDefaultProps(props, muiComponentConfig); - const minDate = minDateProp || utils.date(defaultMinDate); - const maxDate = maxDateProp || utils.date(defaultMaxDate); + const utils = useUtils(); + const classes = useStyles(); + const isStatic = React.useContext(IsStaticVariantContext); + const allowKeyboardControl = allowKeyboardControlProp ?? !isStatic; - const { - calendarState, - changeFocusedDay, - changeMonth, - isDateDisabled, - handleChangeMonth, - onMonthSwitchingAnimationEnd, - } = useCalendarState({ - date, - reduceAnimations, - onMonthChange, - minDate, - maxDate, - shouldDisableDate, - disablePast, - disableFuture, - }); + const minDate = minDateProp || utils.date(defaultMinDate)!; + const maxDate = maxDateProp || utils.date(defaultMaxDate)!; - React.useEffect(() => { - if (date && isDateDisabled(date)) { - const closestEnabledDate = findClosestEnabledDate({ - utils, - date, - minDate, - maxDate, - disablePast: Boolean(disablePast), - disableFuture: Boolean(disableFuture), - shouldDisableDate: isDateDisabled, - }); + const { + calendarState, + changeFocusedDay, + changeMonth, + isDateDisabled, + handleChangeMonth, + onMonthSwitchingAnimationEnd, + } = useCalendarState({ + date, + reduceAnimations, + onMonthChange, + minDate, + maxDate, + shouldDisableDate, + disablePast, + disableFuture, + }); - onChange(closestEnabledDate, 'partial'); - } - // This call is too expensive to run it on each prop change. - // So just ensure that we are not rendering disabled as selected on mount. - }, []); // eslint-disable-line + React.useEffect(() => { + if (date && isDateDisabled(date)) { + const closestEnabledDate = findClosestEnabledDate({ + utils, + date, + minDate, + maxDate, + disablePast: Boolean(disablePast), + disableFuture: Boolean(disableFuture), + shouldDisableDate: isDateDisabled, + }); - React.useEffect(() => { - changeMonth(date); - }, [date]); // eslint-disable-line + onChange(closestEnabledDate, 'partial'); + } + // This call is too expensive to run it on each prop change. + // So just ensure that we are not rendering disabled as selected on mount. + }, []); // eslint-disable-line - return ( - - handleChangeMonth({ newMonth, direction })} - minDate={minDate} - maxDate={maxDate} - disablePast={disablePast} - disableFuture={disableFuture} - reduceAnimations={reduceAnimations} - /> - -
- {view === 'year' && ( - - )} - {view === 'month' && ( - - )} - {view === 'date' && ( - - )} -
-
-
- ); - } -); + React.useEffect(() => { + changeMonth(date); + }, [date]); // eslint-disable-line + + return ( + + handleChangeMonth({ newMonth, direction })} + minDate={minDate} + maxDate={maxDate} + disablePast={disablePast} + disableFuture={disableFuture} + reduceAnimations={reduceAnimations} + /> + +
+ {view === 'year' && ( + + )} + {view === 'month' && ( + + )} + {view === 'date' && ( + + )} +
+
+
+ ); +} diff --git a/lib/src/views/Calendar/Day.tsx b/lib/src/views/Calendar/Day.tsx index 1d4117e3ff9cb4..e31c31cb0f0670 100644 --- a/lib/src/views/Calendar/Day.tsx +++ b/lib/src/views/Calendar/Day.tsx @@ -7,7 +7,7 @@ import { ExtendMui } from '../../typings/helpers'; import { onSpaceOrEnter } from '../../_helpers/utils'; import { useUtils } from '../../_shared/hooks/useUtils'; import { DAY_SIZE, DAY_MARGIN } from '../../constants/dimensions'; -import { withDefaultProps } from '../../_shared/withDefaultProps'; +import { useDefaultProps } from '../../_shared/withDefaultProps'; import { useCanAutoFocus } from '../../_shared/hooks/useCanAutoFocus'; import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; @@ -73,11 +73,11 @@ export const useStyles = makeStyles( muiComponentConfig ); -export interface DayProps extends ExtendMui { +export interface DayProps extends ExtendMui { /** * The date to show. */ - day: unknown; + day: TDate; /** * Is focused by keyboard navigation. */ @@ -132,11 +132,11 @@ export interface DayProps extends ExtendMui { * @default false */ allowSameDateSelection?: boolean; - onDayFocus: (day: unknown) => void; - onDaySelect: (day: unknown, isFinish: PickerSelectionState) => void; + onDayFocus?: (day: TDate) => void; + onDaySelect: (day: TDate, isFinish: PickerSelectionState) => void; } -const PureDay: React.FC = (props) => { +function PureDay(props: DayProps) { const { allowKeyboardControl, allowSameDateSelection = false, @@ -159,7 +159,8 @@ const PureDay: React.FC = (props) => { showDaysOutsideCurrentMonth = false, today: isToday = false, ...other - } = props; + } = useDefaultProps(props, muiComponentConfig); + const utils = useUtils(); const classes = useStyles(); const canAutoFocus = useCanAutoFocus(); @@ -180,7 +181,7 @@ const PureDay: React.FC = (props) => { }, [allowKeyboardControl, canAutoFocus, disabled, focused, isAnimating, isInCurrentMonth]); const handleFocus = (event: React.FocusEvent) => { - if (!focused) { + if (!focused && onDayFocus) { onDayFocus(day); } @@ -240,9 +241,9 @@ const PureDay: React.FC = (props) => { {utils.format(day, 'dayOfMonth')} ); -}; +} -export const areDayPropsEqual = (prevProps: DayProps, nextProps: DayProps) => { +export const areDayPropsEqual = (prevProps: DayProps, nextProps: DayProps) => { return ( prevProps.focused === nextProps.focused && prevProps.focusable === nextProps.focusable && @@ -269,4 +270,5 @@ PureDay.propTypes = { today: PropTypes.bool, }; -export const Day = withDefaultProps(muiComponentConfig, React.memo(PureDay, areDayPropsEqual)); +// keep typings of original component and not loose generic +export const Day = (React.memo(PureDay, areDayPropsEqual) as unknown) as typeof PureDay; diff --git a/lib/src/views/Calendar/MonthSelection.tsx b/lib/src/views/Calendar/MonthSelection.tsx index dcca1a2f2412df..c98dd75ab737b5 100644 --- a/lib/src/views/Calendar/MonthSelection.tsx +++ b/lib/src/views/Calendar/MonthSelection.tsx @@ -1,18 +1,17 @@ import * as React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import { Month } from './Month'; -import { useUtils } from '../../_shared/hooks/useUtils'; -import { ParsableDate } from '../../constants/prop-types'; +import { useUtils, useNow } from '../../_shared/hooks/useUtils'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -export interface MonthSelectionProps { - date: unknown; - minDate?: ParsableDate; - maxDate?: ParsableDate; - onChange: PickerOnChangeFn; +export interface MonthSelectionProps { + date: TDate | null; + minDate: TDate; + maxDate: TDate; + onChange: PickerOnChangeFn; disablePast?: boolean | null | undefined; disableFuture?: boolean | null | undefined; - onMonthChange?: (date: unknown) => void | Promise; + onMonthChange?: (date: TDate) => void | Promise; } export const useStyles = makeStyles( @@ -27,7 +26,7 @@ export const useStyles = makeStyles( { name: 'MuiPickersMonthSelection' } ); -export const MonthSelection: React.FC = ({ +export function MonthSelection({ date, disableFuture, disablePast, @@ -35,22 +34,19 @@ export const MonthSelection: React.FC = ({ minDate, onChange, onMonthChange, -}) => { - const utils = useUtils(); +}: MonthSelectionProps) { + const utils = useUtils(); + const now = useNow(); const classes = useStyles(); - const currentMonth = utils.getMonth(date); - - const shouldDisableMonth = (month: unknown) => { - const now = utils.date(); - const utilMinDate = utils.date(minDate); - const utilMaxDate = utils.date(maxDate); + const currentMonth = utils.getMonth(date || now); + const shouldDisableMonth = (month: TDate) => { const firstEnabledMonth = utils.startOfMonth( - disablePast && utils.isAfter(now, utilMinDate) ? now : utilMinDate + disablePast && utils.isAfter(now, minDate) ? now : minDate ); const lastEnabledMonth = utils.startOfMonth( - disableFuture && utils.isBefore(now, utilMaxDate) ? now : utilMaxDate + disableFuture && utils.isBefore(now, maxDate) ? now : maxDate ); const isBeforeFirstEnabled = utils.isBefore(month, firstEnabledMonth); @@ -61,19 +57,19 @@ export const MonthSelection: React.FC = ({ const onMonthSelect = React.useCallback( (month: number) => { - const newDate = utils.setMonth(date, month); + const newDate = utils.setMonth(date || now, month); onChange(newDate, 'finish'); if (onMonthChange) { onMonthChange(newDate); } }, - [date, onChange, onMonthChange, utils] + [date, now, onChange, onMonthChange, utils] ); return (
- {utils.getMonthArray(date).map((month) => { + {utils.getMonthArray(date || now).map((month) => { const monthNumber = utils.getMonth(month); const monthText = utils.format(month, 'monthShort'); @@ -91,4 +87,4 @@ export const MonthSelection: React.FC = ({ })}
); -}; +} diff --git a/lib/src/views/Calendar/YearSelection.tsx b/lib/src/views/Calendar/YearSelection.tsx index 9c2b487266fd84..671b813c5585dc 100644 --- a/lib/src/views/Calendar/YearSelection.tsx +++ b/lib/src/views/Calendar/YearSelection.tsx @@ -9,7 +9,7 @@ import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; import { WrapperVariantContext } from '../../wrappers/WrapperVariantContext'; import { useGlobalKeyDown, keycode as keys } from '../../_shared/hooks/useKeyDown'; -export interface ExportedYearSelectionProps { +export interface ExportedYearSelectionProps { /** * Callback firing on year change @DateIOType. */ @@ -21,16 +21,16 @@ export interface ExportedYearSelectionProps { shouldDisableYear?: (day: unknown) => boolean; } -export interface YearSelectionProps extends ExportedYearSelectionProps { +export interface YearSelectionProps extends ExportedYearSelectionProps { allowKeyboardControl?: boolean; - changeFocusedDay: (day: unknown) => void; - date: unknown; + changeFocusedDay: (day: TDate) => void; + date: TDate; disableFuture?: boolean | null | undefined; disablePast?: boolean | null | undefined; - isDateDisabled: (day: unknown) => boolean; - maxDate: unknown; - minDate: unknown; - onChange: PickerOnChangeFn; + isDateDisabled: (day: TDate) => boolean; + maxDate: TDate; + minDate: TDate; + onChange: PickerOnChangeFn; } export const useStyles = makeStyles( @@ -47,7 +47,7 @@ export const useStyles = makeStyles( { name: 'MuiPickersYearSelection' } ); -export const YearSelection: React.FC = ({ +export function YearSelection({ allowKeyboardControl, changeFocusedDay, date: __dateOrNull, @@ -59,10 +59,10 @@ export const YearSelection: React.FC = ({ onChange, onYearChange, shouldDisableYear, -}) => { - const now = useNow(); +}: YearSelectionProps) { + const now = useNow(); const theme = useTheme(); - const utils = useUtils(); + const utils = useUtils(); const classes = useStyles(); const selectedDate = __dateOrNull || now; @@ -73,9 +73,9 @@ export const YearSelection: React.FC = ({ const handleYearSelection = React.useCallback( (year: number, isFinish: PickerSelectionState = 'finish') => { - const submitDate = (newDate: unknown) => { + const submitDate = (newDate: TDate | null) => { onChange(newDate, isFinish); - changeFocusedDay(newDate); + changeFocusedDay(newDate || now); if (onYearChange) { onYearChange(newDate); @@ -101,6 +101,7 @@ export const YearSelection: React.FC = ({ }, [ utils, + now, selectedDate, isDateDisabled, onChange, @@ -158,4 +159,4 @@ export const YearSelection: React.FC = ({ })} ); -}; +} diff --git a/lib/src/views/Calendar/useCalendarState.tsx b/lib/src/views/Calendar/useCalendarState.tsx index 0be61ae38b6fac..7cc3a294b43a90 100644 --- a/lib/src/views/Calendar/useCalendarState.tsx +++ b/lib/src/views/Calendar/useCalendarState.tsx @@ -4,31 +4,31 @@ import { SlideDirection } from './SlideTransition'; import { validateDate } from '../../_helpers/date-utils'; import { MuiPickersAdapter, useUtils, useNow } from '../../_shared/hooks/useUtils'; -interface CalendarState { +interface CalendarState { isMonthSwitchingAnimating: boolean; - currentMonth: unknown; - focusedDay: unknown | null; + currentMonth: TDate; + focusedDay: TDate; slideDirection: SlideDirection; } type ReducerAction = { type: TType } & TAdditional; -interface ChangeMonthPayload { +interface ChangeMonthPayload { direction: SlideDirection; - newMonth: unknown; + newMonth: TDate; } -export const createCalendarStateReducer = ( +export const createCalendarStateReducer = ( reduceAnimations: boolean, disableSwitchToMonthOnDayFocus: boolean, - utils: MuiPickersAdapter + utils: MuiPickersAdapter ) => ( - state: CalendarState, + state: CalendarState, action: | ReducerAction<'finishMonthSwitchingAnimation'> - | ReducerAction<'changeMonth', ChangeMonthPayload> - | ReducerAction<'changeFocusedDay', { focusedDay: unknown }> -): CalendarState => { + | ReducerAction<'changeMonth', ChangeMonthPayload> + | ReducerAction<'changeFocusedDay', { focusedDay: TDate }> +): CalendarState => { switch (action.type) { case 'changeMonth': return { @@ -64,8 +64,8 @@ export const createCalendarStateReducer = ( } }; -type CalendarStateInput = Pick< - CalendarViewProps, +type CalendarStateInput = Pick< + CalendarViewProps, | 'disableFuture' | 'disablePast' | 'shouldDisableDate' @@ -73,12 +73,12 @@ type CalendarStateInput = Pick< | 'reduceAnimations' | 'onMonthChange' > & { - minDate: unknown; - maxDate: unknown; + minDate: TDate; + maxDate: TDate; disableSwitchToMonthOnDayFocus?: boolean; }; -export function useCalendarState({ +export function useCalendarState({ date, disableFuture, disablePast, @@ -88,9 +88,9 @@ export function useCalendarState({ onMonthChange, reduceAnimations, shouldDisableDate, -}: CalendarStateInput) { - const now = useNow(); - const utils = useUtils(); +}: CalendarStateInput) { + const now = useNow(); + const utils = useUtils(); const dateForMonth = date || now; const reducerFn = React.useRef( createCalendarStateReducer(Boolean(reduceAnimations), disableSwitchToMonthOnDayFocus, utils) @@ -104,7 +104,7 @@ export function useCalendarState({ }); const handleChangeMonth = React.useCallback( - (payload: ChangeMonthPayload) => { + (payload: ChangeMonthPayload) => { dispatch({ type: 'changeMonth', ...payload, @@ -118,7 +118,7 @@ export function useCalendarState({ ); const changeMonth = React.useCallback( - (newDate: unknown) => { + (newDate: TDate) => { const newDateRequested = newDate ?? now; if (utils.isSameMonth(newDateRequested, calendarState.currentMonth)) { return; @@ -135,7 +135,7 @@ export function useCalendarState({ ); const isDateDisabled = React.useCallback( - (day: unknown) => + (day: TDate | null) => validateDate(utils, day, { disablePast, disableFuture, @@ -151,7 +151,7 @@ export function useCalendarState({ }, []); const changeFocusedDay = React.useCallback( - (newFocusedDate: unknown) => { + (newFocusedDate: TDate) => { if (!isDateDisabled(newFocusedDate)) { dispatch({ type: 'changeFocusedDay', focusedDay: newFocusedDate }); } diff --git a/lib/src/views/Clock/Clock.tsx b/lib/src/views/Clock/Clock.tsx index 90c81199ab6bae..a6c2d07d76f409 100644 --- a/lib/src/views/Clock/Clock.tsx +++ b/lib/src/views/Clock/Clock.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import * as PropTypes from 'prop-types'; import clsx from 'clsx'; import IconButton from '@material-ui/core/IconButton'; import Typography from '@material-ui/core/Typography'; @@ -9,19 +10,19 @@ import { VIEW_HEIGHT } from '../../constants/dimensions'; import { ClockViewType } from '../../constants/ClockType'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; import { getHours, getMinutes } from '../../_helpers/time-utils'; -import { withDefaultProps } from '../../_shared/withDefaultProps'; +import { useDefaultProps } from '../../_shared/withDefaultProps'; import { useMeridiemMode } from '../../TimePicker/TimePickerToolbar'; import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; import { useGlobalKeyDown, keycode } from '../../_shared/hooks/useKeyDown'; import { WrapperVariantContext } from '../../wrappers/WrapperVariantContext'; -export interface ClockProps extends ReturnType { - date: unknown; +export interface ClockProps extends ReturnType { + date: TDate | null; type: ClockViewType; value: number; isTimeDisabled: (timeValue: number, type: ClockViewType) => boolean; children: React.ReactElement[]; - onDateChange: PickerOnChangeFn; + onDateChange: PickerOnChangeFn; onChange: (value: number, isFinish?: PickerSelectionState) => void; ampm?: boolean; minutesStep?: number; @@ -95,7 +96,7 @@ export const useStyles = makeStyles( muiComponentConfig ); -export const Clock: React.FC = withDefaultProps(muiComponentConfig, (props) => { +export function Clock(props: ClockProps) { const { allowKeyboardControl, ampm, @@ -109,7 +110,8 @@ export const Clock: React.FC = withDefaultProps(muiComponentConfig, onChange, type, value, - } = props; + } = useDefaultProps(props, muiComponentConfig); + const utils = useUtils(); const classes = useStyles(); const wrapperVariant = React.useContext(WrapperVariantContext); @@ -250,6 +252,11 @@ export const Clock: React.FC = withDefaultProps(muiComponentConfig, )} ); -}); +} + +Clock.propTypes = { + ampm: PropTypes.bool, + minutesStep: PropTypes.number, +} as any; Clock.displayName = 'Clock'; diff --git a/lib/src/views/Clock/ClockView.tsx b/lib/src/views/Clock/ClockView.tsx index 8fdd1346cea792..1cce82f6c3e6da 100644 --- a/lib/src/views/Clock/ClockView.tsx +++ b/lib/src/views/Clock/ClockView.tsx @@ -5,7 +5,7 @@ import { Clock } from './Clock'; import { pipe } from '../../_helpers/utils'; import { useUtils, useNow } from '../../_shared/hooks/useUtils'; import { PickerOnChangeFn } from '../../_shared/hooks/useViews'; -import { withDefaultProps } from '../../_shared/withDefaultProps'; +import { useDefaultProps } from '../../_shared/withDefaultProps'; import { getHourNumbers, getMinutesNumbers } from './ClockNumbers'; import { useMeridiemMode } from '../../TimePicker/TimePickerToolbar'; import { PickerSelectionState } from '../../_shared/hooks/usePickerState'; @@ -16,7 +16,7 @@ import { TimeValidationProps, } from '../../_helpers/time-utils'; -export interface ExportedClockViewProps extends TimeValidationProps { +export interface ExportedClockViewProps extends TimeValidationProps { /** * 12h/24h view for hour selection clock. * @@ -43,11 +43,13 @@ export interface ExportedClockViewProps extends TimeValidationProps { allowKeyboardControl?: boolean; } -export interface ClockViewProps extends ExportedClockViewProps, ExportedArrowSwitcherProps { +export interface ClockViewProps + extends ExportedClockViewProps, + ExportedArrowSwitcherProps { /** * Selected date @DateIOType. */ - date: unknown; + date: TDate | null; /** * Clock type. */ @@ -55,11 +57,11 @@ export interface ClockViewProps extends ExportedClockViewProps, ExportedArrowSwi /** * On change date without moving between views @DateIOType. */ - onDateChange: PickerOnChangeFn; + onDateChange: PickerOnChangeFn; /** * On change callback @DateIOType. */ - onChange: PickerOnChangeFn; + onChange: PickerOnChangeFn; /** * Get clock number aria-text for hours. */ @@ -96,15 +98,11 @@ function getMinutesAriaText(minute: string) { return `${minute} minutes`; } -function getHoursAriaText(hour: string) { - return `${hour} hours`; -} +const getHoursAriaText = (hour: string) => `${hour} hours`; -function getSecondsAriaText(seconds: string) { - return `${seconds} seconds`; -} +const getSecondsAriaText = (seconds: string) => `${seconds} seconds`; -function ClockViewRaw(props: ClockViewProps) { +export function ClockView(props: ClockViewProps) { const { allowKeyboardControl, ampm, @@ -132,15 +130,26 @@ function ClockViewRaw(props: ClockViewProps) { shouldDisableTime, showViewSwitcher, type, - } = props; - const now = useNow(); - const utils = useUtils(); + } = useDefaultProps(props, muiPickersComponentConfig); + + const now = useNow(); + const utils = useUtils(); const classes = useStyles(); - const { meridiemMode, handleMeridiemChange } = useMeridiemMode(date, ampm, onDateChange); + const dateOrNow = date || now; + + const { meridiemMode, handleMeridiemChange } = useMeridiemMode( + dateOrNow, + ampm, + onDateChange + ); const isTimeDisabled = React.useCallback( (rawValue: number, type: 'hours' | 'minutes' | 'seconds') => { - const validateTimeValue = (getRequestedTimePoint: (when: 'start' | 'end') => unknown) => { + if (date === null) { + return false; + } + + const validateTimeValue = (getRequestedTimePoint: (when: 'start' | 'end') => TDate) => { const isAfterComparingFn = createIsAfterIgnoreDatePart( Boolean(disableIgnoringDatePartForTimeValidation), utils @@ -192,7 +201,6 @@ function ClockViewRaw(props: ClockViewProps) { ] ); - const dateOrNow = date || now; const viewProps = React.useMemo(() => { switch (type) { case 'hours': { @@ -287,9 +295,11 @@ function ClockViewRaw(props: ClockViewProps) { isRightDisabled={nextViewAvailable} /> )} +