diff --git a/docs/layout/components/navigationMap.ts b/docs/layout/components/navigationMap.ts index 3e4121d083f00d..0142e347fe52af 100644 --- a/docs/layout/components/navigationMap.ts +++ b/docs/layout/components/navigationMap.ts @@ -21,6 +21,7 @@ export const navItems = [ title: 'Components Demo', children: [ { title: 'Date Picker', href: '/demo/datepicker' }, + { title: 'Date Range Picker', href: '/demo/daterangepicker' }, { title: 'Time Picker', href: '/demo/timepicker' }, { title: 'Date & Time Picker', href: '/demo/datetime-picker' }, ], diff --git a/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.jsx b/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.jsx new file mode 100644 index 00000000000000..b6ffc812372e98 --- /dev/null +++ b/docs/pages/demo/daterangepicker/BasicDateRangePicker.example.jsx @@ -0,0 +1,10 @@ +import React, { useState } from 'react'; +import { DatePicker as DateRangePicker } from '@material-ui/pickers'; + +function BasicDateRangePicker() { + const [selectedDate, handleDateChange] = useState([new Date(), null]); + + return handleDateChange(date)} />; +} + +export default BasicDateRangePicker; diff --git a/docs/pages/demo/daterangepicker/index.mdx b/docs/pages/demo/daterangepicker/index.mdx new file mode 100644 index 00000000000000..d1c14f72b487f8 --- /dev/null +++ b/docs/pages/demo/daterangepicker/index.mdx @@ -0,0 +1,20 @@ +import Ad from '_shared/Ad'; +import Example from '_shared/Example'; +import PageMeta from '_shared/PageMeta'; +import LinkedComponents from '_shared/LinkedComponents'; + +import * as BasicDateRangePicker from './BasicDateRangePicker.example'; + + + +## Date picker + +[Date pickers](https://material.io/components/pickers/) let users select a date, or a range of dates. They should be suitable for the context in which they appear. + + + +#### Basic usage + +Will be rendered to modal dialog on mobile and textfield with popover on desktop. + + diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 1f74c13426f1dc..bd91fd82a45bb1 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -19,5 +19,5 @@ "target": "esnext", "resolveJsonModule": true }, - "include": ["../docs/**/*.ts*", "../lib/typings.d.ts"] + "include": ["../docs/**/*.ts*", "./typings.d.ts", "../lib/typings.d.ts"] } diff --git a/lib/.size-snapshot.json b/lib/.size-snapshot.json index adf769a8f540aa..aeefd56ac73a5b 100644 --- a/lib/.size-snapshot.json +++ b/lib/.size-snapshot.json @@ -1,26 +1,26 @@ { "build/dist/material-ui-pickers.esm.js": { - "bundled": 143471, - "minified": 78671, - "gzipped": 21103, + "bundled": 145384, + "minified": 79576, + "gzipped": 21474, "treeshaked": { "rollup": { - "code": 64934, + "code": 65692, "import_statements": 2099 }, "webpack": { - "code": 72410 + "code": 73171 } } }, "build/dist/material-ui-pickers.umd.js": { - "bundled": 597704, - "minified": 222450, - "gzipped": 45688 + "bundled": 599635, + "minified": 223221, + "gzipped": 45558 }, "build/dist/material-ui-pickers.umd.min.js": { - "bundled": 537438, - "minified": 203944, - "gzipped": 40910 + "bundled": 539383, + "minified": 204705, + "gzipped": 40705 } } diff --git a/lib/src/DatePicker/DatePicker.tsx b/lib/src/DatePicker/DatePicker.tsx index fa3b978dd938f9..60e997fc507cae 100644 --- a/lib/src/DatePicker/DatePicker.tsx +++ b/lib/src/DatePicker/DatePicker.tsx @@ -1,29 +1,18 @@ import { useUtils } from '../_shared/hooks/useUtils'; -import { MaterialUiPickersDate } from '../typings/date'; import { DatePickerToolbar } from './DatePickerToolbar'; import { getFormatByViews } from '../_helpers/date-utils'; +import { WithViewsProps } from '../Picker/SharedPickerProps'; import { datePickerDefaultProps } from '../constants/prop-types'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; import { ExportedCalendarViewProps } from '../views/Calendar/CalendarView'; +import { makePickerWithStateAndWrapper } from '../Picker/makePickerWithState'; import { ModalWrapper, InlineWrapper, StaticWrapper } from '../wrappers/Wrapper'; -import { - WithDateInputProps, - makePickerWithStateAndWrapper, - WithViewsProps, -} from '../Picker/makePickerWithState'; export type DatePickerView = 'year' | 'date' | 'month'; -export interface BaseDatePickerProps extends ExportedCalendarViewProps { - /** Callback firing on year change @DateIOType */ - onYearChange?: (date: MaterialUiPickersDate) => void; - /** Date format, that is displaying in toolbar */ - toolbarFormat?: string; -} - -export type DatePickerProps = BaseDatePickerProps & - WithDateInputProps & - WithViewsProps<'year' | 'date' | 'month'>; +export interface DatePickerProps + extends WithViewsProps<'year' | 'date' | 'month'>, + ExportedCalendarViewProps {} const datePickerConfig = { DefaultToolbarComponent: DatePickerToolbar, diff --git a/lib/src/DateTimePicker/DateTimePicker.tsx b/lib/src/DateTimePicker/DateTimePicker.tsx index 2049c9b67bc4e1..3bd8c1bb018efd 100644 --- a/lib/src/DateTimePicker/DateTimePicker.tsx +++ b/lib/src/DateTimePicker/DateTimePicker.tsx @@ -1,22 +1,20 @@ import { useUtils } from '../_shared/hooks/useUtils'; -import { BaseDatePickerProps } from '../DatePicker/DatePicker'; import { DateTimePickerToolbar } from './DateTimePickerToolbar'; import { ExportedClockViewProps } from '../views/Clock/ClockView'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; import { pick12hOr24hFormat } from '../_helpers/text-field-helper'; +import { ExportedCalendarViewProps } from '../views/Calendar/CalendarView'; +import { makePickerWithStateAndWrapper } from '../Picker/makePickerWithState'; import { InlineWrapper, ModalWrapper, StaticWrapper } from '../wrappers/Wrapper'; +import { WithViewsProps, AllSharedPickerProps } from '../Picker/SharedPickerProps'; import { dateTimePickerDefaultProps, ParsableDate } from '../constants/prop-types'; -import { - makePickerWithStateAndWrapper, - WithDateInputProps, - WithViewsProps, -} from '../Picker/makePickerWithState'; export type DateTimePickerView = 'year' | 'date' | 'month' | 'hours' | 'minutes' | 'seconds'; -export type BaseDateTimePickerProps = ExportedClockViewProps & BaseDatePickerProps; - -export interface DateTimePickerViewsProps extends BaseDateTimePickerProps { +export interface DateTimePickerProps + extends WithViewsProps<'year' | 'date' | 'month' | 'hours' | 'minutes'>, + ExportedClockViewProps, + ExportedCalendarViewProps { /** To show tabs */ hideTabs?: boolean; /** Date tab icon */ @@ -31,10 +29,6 @@ export interface DateTimePickerViewsProps extends BaseDateTimePickerProps { toolbarFormat?: string; } -export type DateTimePickerProps = WithDateInputProps & - DateTimePickerViewsProps & - WithViewsProps<'year' | 'date' | 'month' | 'hours' | 'minutes'>; - function useDefaultProps({ ampm, mask, @@ -44,7 +38,7 @@ function useDefaultProps({ orientation = 'portrait', openTo = 'date', views = ['year', 'date', 'hours', 'minutes'], -}: DateTimePickerProps) { +}: DateTimePickerProps & AllSharedPickerProps) { const utils = useUtils(); const willUseAmPm = ampm ?? utils.is12HourCycleInCurrentLocale(); diff --git a/lib/src/LocalizationProvider.tsx b/lib/src/LocalizationProvider.tsx index fdf6c2ad95bbca..f3b77ab7e4b312 100644 --- a/lib/src/LocalizationProvider.tsx +++ b/lib/src/LocalizationProvider.tsx @@ -29,13 +29,12 @@ export const LocalizationProvider: React.FC = ({ }; LocalizationProvider.propTypes = { - // @ts-ignore dateAdapter: PropTypes.func.isRequired, locale: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), children: PropTypes.oneOfType([ PropTypes.element.isRequired, PropTypes.arrayOf(PropTypes.element.isRequired), ]).isRequired, -}; +} as any; export default LocalizationProvider; diff --git a/lib/src/Picker/Picker.tsx b/lib/src/Picker/Picker.tsx index 4a6d2ee35ed8f2..558d0172847744 100644 --- a/lib/src/Picker/Picker.tsx +++ b/lib/src/Picker/Picker.tsx @@ -4,60 +4,63 @@ import { WrapperVariant } from '../wrappers/Wrapper'; import { useViews } from '../_shared/hooks/useViews'; import { makeStyles } from '@material-ui/core/styles'; import { DateTimePickerView } from '../DateTimePicker'; -import { WithViewsProps } from './makePickerWithState'; +import { ParsableDate } from '../constants/prop-types'; import { BasePickerProps } from '../typings/BasePicker'; import { MaterialUiPickersDate } from '../typings/date'; import { DateInputProps } from '../_shared/PureDateInput'; -import { CalendarView } from '../views/Calendar/CalendarView'; +import { DatePickerView } from '../DatePicker/DatePicker'; import { useIsLandscape } from '../_shared/hooks/useIsLandscape'; +import { WithViewsProps, AnyPickerView } from './SharedPickerProps'; import { DIALOG_WIDTH, VIEW_HEIGHT } from '../constants/dimensions'; import { WrapperVariantContext } from '../wrappers/WrapperVariantContext'; import { MobileKeyboardInputView } from '../views/MobileKeyboardInputView'; import { ClockView, ExportedClockViewProps } from '../views/Clock/ClockView'; -import { BaseDatePickerProps, DatePickerView } from '../DatePicker/DatePicker'; +import { CalendarView, ExportedCalendarViewProps } from '../views/Calendar/CalendarView'; -export type PickerView = DateTimePickerView; +type CalendarAndClockProps = ExportedCalendarViewProps & ExportedClockViewProps; -export type ToolbarComponentProps = BaseDatePickerProps & - ExportedClockViewProps & { - views: T[]; - openView: T; - date: MaterialUiPickersDate; - setOpenView: (view: T) => void; - onChange: (date: MaterialUiPickersDate, isFinish?: boolean) => void; - toolbarTitle?: string; - // TODO move out, cause it is DateTimePickerOnly - hideTabs?: boolean; - dateRangeIcon?: React.ReactNode; - timeIcon?: React.ReactNode; - isLandscape: boolean; - ampmInClock?: boolean; - isMobileKeyboardViewOpen: boolean; - toggleMobileKeyboardView: () => void; - getMobileKeyboardInputViewButtonText?: () => string; - }; +export type ToolbarComponentProps< + T extends AnyPickerView = AnyPickerView +> = CalendarAndClockProps & { + views: T[]; + openView: T; + date: MaterialUiPickersDate; + setOpenView: (view: T) => void; + onChange: (date: MaterialUiPickersDate, isFinish?: boolean) => void; + toolbarTitle?: React.ReactNode; + toolbarFormat?: string; + // TODO move out, cause it is DateTimePickerOnly + hideTabs?: boolean; + dateRangeIcon?: React.ReactNode; + timeIcon?: React.ReactNode; + isLandscape: boolean; + ampmInClock?: boolean; + isMobileKeyboardViewOpen: boolean; + toggleMobileKeyboardView: () => void; + getMobileKeyboardInputViewButtonText?: () => string; +}; -export interface PickerViewProps +export interface ExportedPickerProps extends Omit, - WithViewsProps, - BaseDatePickerProps, - ExportedClockViewProps { - toolbarTitle?: string; - showToolbar?: boolean; - ToolbarComponent: React.ComponentType>; + CalendarAndClockProps, + WithViewsProps { // TODO move out, cause it is DateTimePickerOnly hideTabs?: boolean; dateRangeIcon?: React.ReactNode; timeIcon?: React.ReactNode; } -interface PickerProps extends PickerViewProps { +export interface PickerProps< + TView extends AnyPickerView, + TInputValue = ParsableDate, + TDateValue = MaterialUiPickersDate +> extends ExportedPickerProps { isMobileKeyboardViewOpen: boolean; toggleMobileKeyboardView: () => void; - DateInputProps: DateInputProps; - date: MaterialUiPickersDate; + DateInputProps: DateInputProps; + date: TDateValue | null; onDateChange: ( - date: MaterialUiPickersDate, + date: TDateValue, currentVariant: WrapperVariant, isFinish?: boolean | symbol ) => void; @@ -97,13 +100,14 @@ export function Picker({ toolbarTitle, showToolbar, onDateChange, - ToolbarComponent, + ToolbarComponent = () => null, orientation, DateInputProps, isMobileKeyboardViewOpen, toggleMobileKeyboardView, + toolbarFormat, ...other -}: PickerProps) { +}: PickerProps) { const classes = useStyles(); const isLandscape = useIsLandscape(views, orientation); const wrapperVariant = React.useContext(WrapperVariantContext); @@ -141,7 +145,7 @@ export function Picker({ setOpenView={setOpenView} openView={openView} toolbarTitle={toolbarTitle} - ampmInClock={other.ampmInClock} + toolbarFormat={toolbarFormat} isMobileKeyboardViewOpen={isMobileKeyboardViewOpen} toggleMobileKeyboardView={toggleMobileKeyboardView} /> diff --git a/lib/src/Picker/SharedPickerProps.tsx b/lib/src/Picker/SharedPickerProps.tsx new file mode 100644 index 00000000000000..53b57f92b7331f --- /dev/null +++ b/lib/src/Picker/SharedPickerProps.tsx @@ -0,0 +1,23 @@ +import { DateTimePickerView } from '../DateTimePicker'; +import { BasePickerProps } from '../typings/BasePicker'; +import { ExportedDateInputProps } from '../_shared/PureDateInput'; +import { DateValidationProps } from '../_helpers/text-field-helper'; +import { WithDateAdapterProps } from '../_shared/withDateAdapterProp'; + +export type AnyPickerView = DateTimePickerView; + +export type AllSharedPickerProps = WithDateAdapterProps & + BasePickerProps & + ExportedDateInputProps & + DateValidationProps; + +export interface WithViewsProps { + /** + * Array of views to show + */ + views?: T[]; + /** First view to show */ + openTo?: T; +} + +export type WithDateInputProps = DateValidationProps & BasePickerProps & ExportedDateInputProps; diff --git a/lib/src/Picker/makePickerWithState.tsx b/lib/src/Picker/makePickerWithState.tsx index c9fd186d4ef172..cf77b3dc73ce8a 100644 --- a/lib/src/Picker/makePickerWithState.tsx +++ b/lib/src/Picker/makePickerWithState.tsx @@ -1,67 +1,58 @@ import * as React from 'react'; -import { MakeOptional } from '../typings/helpers'; -import { DateTimePickerView } from '../DateTimePicker'; -import { BasePickerProps } from '../typings/BasePicker'; +import { ParsableDate } from '../constants/prop-types'; +import { MaterialUiPickersDate } from '../typings/date'; +import { PureDateInput } from '../_shared/PureDateInput'; +import { parsePickerInputValue } from '../_helpers/date-utils'; +import { KeyboardDateInput } from '../_shared/KeyboardDateInput'; import { usePickerState } from '../_shared/hooks/usePickerState'; -import { ExportedDateInputProps } from '../_shared/PureDateInput'; -import { DateValidationProps } from '../_helpers/text-field-helper'; -import { ResponsiveWrapperProps } from '../wrappers/ResponsiveWrapper'; -import { Picker, ToolbarComponentProps, PickerViewProps } from './Picker'; -import { SomeWrapper, ExtendWrapper, OmitInnerWrapperProps } from '../wrappers/Wrapper'; -import { withDateAdapterProp, WithDateAdapterProps } from '../_shared/withDateAdapterProp'; +import { SomeWrapper, ExtendWrapper } from '../wrappers/Wrapper'; +import { validateDateValue } from '../_helpers/text-field-helper'; +import { withDateAdapterProp } from '../_shared/withDateAdapterProp'; +import { makeWrapperComponent } from '../wrappers/makeWrapperComponent'; +import { AnyPickerView, AllSharedPickerProps } from './SharedPickerProps'; +import { Picker, ToolbarComponentProps, ExportedPickerProps } from './Picker'; -export interface WithViewsProps { - /** - * Array of views to show - */ - views?: T[]; - /** First view to show */ - openTo?: T; -} - -export type WithDateInputProps = DateValidationProps & BasePickerProps & ExportedDateInputProps; +type AllAvailableForOverrideProps = ExportedPickerProps; export interface MakePickerOptions { - useDefaultProps: (props: T) => Partial & { inputFormat?: string }; + useDefaultProps: (props: T & AllSharedPickerProps) => Partial & { inputFormat: string }; DefaultToolbarComponent: React.ComponentType; } -type ExportedPickerProps = MakeOptional, 'ToolbarComponent'>; - export function makePickerWithStateAndWrapper< - T extends ExportedPickerProps & DateValidationProps & Pick, + T extends AllAvailableForOverrideProps, TWrapper extends SomeWrapper = any ->( - Wrapper: TWrapper, - { useDefaultProps, DefaultToolbarComponent }: MakePickerOptions -): React.FC> { - function PickerWithState(props: T & Partial>) { +>(Wrapper: TWrapper, { useDefaultProps, DefaultToolbarComponent }: MakePickerOptions) { + const PickerWrapper = makeWrapperComponent(Wrapper, { + KeyboardDateInputComponent: KeyboardDateInput, + PureDateInputComponent: PureDateInput, + }); + + function PickerWithState(props: T & AllSharedPickerProps & ExtendWrapper) { const defaultProps = useDefaultProps(props); const allProps = { ...defaultProps, ...props }; + const { pickerProps, inputProps, wrapperProps } = usePickerState< + ParsableDate, + MaterialUiPickersDate + >(allProps, parsePickerInputValue, validateDateValue); + const { allowKeyboardControl, ampm, ampmInClock, - autoOk, dateRangeIcon, disableFuture, disablePast, showToolbar, - inputFormat, hideTabs, - defaultHighlight, leftArrowButtonProps, leftArrowIcon, loadingIndicator, maxDate, minDate, minutesStep, - onAccept, - onChange, - onClose, onMonthChange, - onOpen, onYearChange, openTo, orientation, @@ -70,55 +61,24 @@ export function makePickerWithStateAndWrapper< rightArrowIcon, shouldDisableDate, shouldDisableTime, - strictCompareDates, timeIcon, toolbarFormat, ToolbarComponent = DefaultToolbarComponent, - value, views, toolbarTitle, - invalidDateMessage, - minDateMessage, - wider, - showTabs, - maxDateMessage, disableTimeValidationIgnoreDatePart, showDaysOutsideCurrentMonth, disableHighlightToday, - // WrapperProps - clearable, - clearLabel, - DialogProps, - PopoverProps, - okLabel, - cancelLabel, - todayLabel, minTime, maxTime, ...restPropsForTextField } = allProps; - const { pickerProps, inputProps, wrapperProps } = usePickerState(allProps); - const WrapperComponent = Wrapper as SomeWrapper; - return ( - + - + ); } - // @ts-ignore (why prop-types validation is appearing here?) return withDateAdapterProp(PickerWithState); } diff --git a/lib/src/TimePicker/TimePicker.tsx b/lib/src/TimePicker/TimePicker.tsx index 08e27f0f66230c..2bf3c1bc7ae1a2 100644 --- a/lib/src/TimePicker/TimePicker.tsx +++ b/lib/src/TimePicker/TimePicker.tsx @@ -5,18 +5,14 @@ import { ExportedClockViewProps } from '../views/Clock/ClockView'; import { ResponsiveWrapper } from '../wrappers/ResponsiveWrapper'; import { pick12hOr24hFormat } from '../_helpers/text-field-helper'; import { useUtils, MuiPickersAdapter } from '../_shared/hooks/useUtils'; +import { makePickerWithStateAndWrapper } from '../Picker/makePickerWithState'; import { timePickerDefaultProps, ParsableDate } from '../constants/prop-types'; import { ModalWrapper, InlineWrapper, StaticWrapper } from '../wrappers/Wrapper'; -import { - WithDateInputProps, - WithViewsProps, - makePickerWithStateAndWrapper, -} from '../Picker/makePickerWithState'; +import { WithViewsProps, AllSharedPickerProps } from '../Picker/SharedPickerProps'; export interface TimePickerProps extends ExportedClockViewProps, - WithViewsProps<'hours' | 'minutes' | 'seconds'>, - WithDateInputProps {} + WithViewsProps<'hours' | 'minutes' | 'seconds'> {} export function getTextFieldAriaText(value: ParsableDate, utils: MuiPickersAdapter) { return value && utils.isValid(utils.date(value)) @@ -30,7 +26,7 @@ function useDefaultProps({ inputFormat, openTo = 'hours', views = ['hours', 'minutes'], -}: TimePickerProps) { +}: TimePickerProps & AllSharedPickerProps) { const utils = useUtils(); const willUseAmPm = ampm ?? utils.is12HourCycleInCurrentLocale(); diff --git a/lib/src/__tests__/e2e/KeyboardDatePicker.test.tsx b/lib/src/__tests__/e2e/KeyboardDatePicker.test.tsx index 462faf97ce13e2..a690aeada33158 100644 --- a/lib/src/__tests__/e2e/KeyboardDatePicker.test.tsx +++ b/lib/src/__tests__/e2e/KeyboardDatePicker.test.tsx @@ -90,7 +90,7 @@ describe('e2e -- KeyboardDatePicker validation errors', () => { /> ); - expect(component.find('ForwardRef(TextField)').prop('helperText')).toBe(''); + expect(component.find('ForwardRef(TextField)').prop('helperText')).toBe(undefined); }); it('Should render error message if date is after maxDate with strict comparison', () => { @@ -119,7 +119,7 @@ describe('e2e -- KeyboardDatePicker validation errors', () => { /> ); - expect(component.find('ForwardRef(TextField)').prop('helperText')).toBe(''); + expect(component.find('ForwardRef(TextField)').prop('helperText')).toBe(undefined); }); it('Should render error message if date is after minDate with strict comparison', () => { diff --git a/lib/src/_helpers/date-utils.ts b/lib/src/_helpers/date-utils.ts index b10508b9ae4cfe..0ce13a08c9e819 100644 --- a/lib/src/_helpers/date-utils.ts +++ b/lib/src/_helpers/date-utils.ts @@ -1,7 +1,9 @@ import { arrayIncludes } from './utils'; import { IUtils } from '@date-io/core/IUtils'; import { MaterialUiPickersDate } from '../typings/date'; +import { BasePickerProps } from '../typings/BasePicker'; import { DatePickerView } from '../DatePicker/DatePicker'; +import { MuiPickersAdapter } from '../_shared/hooks/useUtils'; interface FindClosestDateParams { date: MaterialUiPickersDate; @@ -94,3 +96,13 @@ export const getFormatByViews = ( return utils.formats.keyboardDate; }; + +export function parsePickerInputValue( + now: MaterialUiPickersDate, + utils: MuiPickersAdapter, + { value, defaultHighlight }: Pick +): MaterialUiPickersDate | null { + const parsedValue = utils.date(value || defaultHighlight || now); + + return parsedValue && utils.isValid(parsedValue) ? parsedValue : now; +} diff --git a/lib/src/_helpers/text-field-helper.ts b/lib/src/_helpers/text-field-helper.ts index aa34a73172e5ee..8021952af022c0 100644 --- a/lib/src/_helpers/text-field-helper.ts +++ b/lib/src/_helpers/text-field-helper.ts @@ -1,4 +1,3 @@ -import { DatePickerProps } from '../DatePicker'; import { ParsableDate } from '../constants/prop-types'; import { MaterialUiPickersDate } from '../typings/date'; import { DateInputProps } from '../_shared/PureDateInput'; @@ -44,6 +43,11 @@ export interface DateValidationProps extends BaseValidationProps { * @default 'Date should not be after maximal date' */ maxDateMessage?: React.ReactNode; + /** + * Compare dates by the exact timestamp, instead of start/end of date + * @default false + */ + strictCompareDates?: boolean; } const getComparisonMaxDate = ( @@ -70,7 +74,7 @@ const getComparisonMinDate = ( return utils.startOfDay(date); }; -export const validate = ( +export const validateDateValue = ( value: ParsableDate, utils: MuiPickersAdapter, { @@ -82,13 +86,13 @@ export const validate = ( minDateMessage, invalidDateMessage, strictCompareDates, - }: Omit + }: any // TODO change the typings when doing hard update of validation system ): React.ReactNode => { const parsedValue = utils.date(value); // if null - do not show error if (value === null) { - return ''; + return undefined; } if (!utils.isValid(value)) { @@ -128,7 +132,7 @@ export const validate = ( return minDateMessage; } - return ''; + return undefined; }; export function pick12hOr24hFormat( diff --git a/lib/src/_shared/PickerToolbar.tsx b/lib/src/_shared/PickerToolbar.tsx index 55897001f0c6cd..d5c6711f9c7a3b 100644 --- a/lib/src/_shared/PickerToolbar.tsx +++ b/lib/src/_shared/PickerToolbar.tsx @@ -48,7 +48,7 @@ interface PickerToolbarProps | 'isMobileKeyboardViewOpen' | 'toggleMobileKeyboardView' > { - toolbarTitle: string; + toolbarTitle: React.ReactNode; landscapeDirection?: 'row' | 'column'; isLandscape: boolean; penIconClassName?: string; diff --git a/lib/src/_shared/PureDateInput.tsx b/lib/src/_shared/PureDateInput.tsx index 5ae150644ad5e8..f1b24d31344d67 100644 --- a/lib/src/_shared/PureDateInput.tsx +++ b/lib/src/_shared/PureDateInput.tsx @@ -9,11 +9,11 @@ import { IconButtonProps } from '@material-ui/core/IconButton'; import { InputAdornmentProps } from '@material-ui/core/InputAdornment'; import { getDisplayDate, getTextFieldAriaText } from '../_helpers/text-field-helper'; -export interface DateInputProps +export interface DateInputProps extends ExtendMui { - rawValue: ParsableDate; + rawValue: TInputValue; inputFormat: string; - onChange: (date: MaterialUiPickersDate | null, keyboardInputValue?: string) => void; + onChange: (date: TDateValue | null, keyboardInputValue?: string) => void; openPicker: () => void; validationError?: React.ReactNode; /** Override input component */ diff --git a/lib/src/_shared/hooks/useOpenState.ts b/lib/src/_shared/hooks/useOpenState.ts index 5af192f80bcb77..c6fbc5e10bcb3f 100644 --- a/lib/src/_shared/hooks/useOpenState.ts +++ b/lib/src/_shared/hooks/useOpenState.ts @@ -2,7 +2,7 @@ import { BasePickerProps } from '../../typings/BasePicker'; import { useCallback, useState, Dispatch, SetStateAction } from 'react'; -export function useOpenState({ open, onOpen, onClose }: BasePickerProps) { +export function useOpenState({ open, onOpen, onClose }: BasePickerProps) { let setIsOpenState: null | Dispatch> = null; if (open === undefined || open === null) { // The component is uncontrolled, so we need to give it its own state. diff --git a/lib/src/_shared/hooks/usePickerState.ts b/lib/src/_shared/hooks/usePickerState.ts index 08a96a8abcb565..49976b2c054676 100644 --- a/lib/src/_shared/hooks/usePickerState.ts +++ b/lib/src/_shared/hooks/usePickerState.ts @@ -1,42 +1,34 @@ -import { useUtils, useNow } from './useUtils'; -import { IUtils } from '@date-io/core/IUtils'; import { useOpenState } from './useOpenState'; import { WrapperVariant } from '../../wrappers/Wrapper'; -import { MaterialUiPickersDate } from '../../typings/date'; import { BasePickerProps } from '../../typings/BasePicker'; -import { validate } from '../../_helpers/text-field-helper'; +import { MaterialUiPickersDate } from '../../typings/date'; +import { useUtils, useNow, MuiPickersAdapter } from './useUtils'; import { useCallback, useDebugValue, useEffect, useMemo, useState } from 'react'; -const useValueToDate = ( - utils: IUtils, - { value, defaultHighlight }: BasePickerProps -) => { - const now = useNow(); - const date = utils.date(value || defaultHighlight || now); - - return date && utils.isValid(date) ? date : now; -}; +export const FORCE_FINISH_PICKER = Symbol('Force closing picker, used for accessibility '); -function useDateValues(props: BasePickerProps) { - const utils = useUtils(); - const date = useValueToDate(utils, props); - const inputFormat = props.inputFormat; +export function usePickerState( + props: BasePickerProps, + parseInputValue: ( + now: MaterialUiPickersDate, + utils: MuiPickersAdapter, + props: BasePickerProps + ) => TOutput | null, + validateInputValue: ( + value: TInput, + utils: MuiPickersAdapter, + props: BasePickerProps + ) => React.ReactNode | undefined +) { + const { autoOk, inputFormat, disabled, readOnly, onAccept, onChange, onError, value } = props; if (!inputFormat) { - throw new Error('format prop is required'); + throw new Error('inputFormat prop is required'); } - return { date, inputFormat }; -} - -export const FORCE_FINISH_PICKER = Symbol('Force closing picker, used for accessibility '); - -export function usePickerState(props: BasePickerProps) { - const { autoOk, disabled, readOnly, onAccept, onChange, onError, value } = props; - - const utils = useUtils(); const now = useNow(); - const { date, inputFormat } = useDateValues(props); + const utils = useUtils(); + const date = parseInputValue(now, utils, props); const [pickerDate, setPickerDate] = useState(date); // Mobile keyboard view is a special case. @@ -52,7 +44,7 @@ export function usePickerState(props: BasePickerProps) { }, [date, isMobileKeyboardViewOpen, isOpen, pickerDate, utils]); const acceptDate = useCallback( - (acceptedDate: MaterialUiPickersDate, needClosePicker: boolean) => { + (acceptedDate: TOutput | null, needClosePicker: boolean) => { onChange(acceptedDate); if (needClosePicker) { @@ -74,8 +66,9 @@ export function usePickerState(props: BasePickerProps) { onAccept: () => acceptDate(pickerDate, true), onDismiss: () => setIsOpen(false), onSetToday: () => { - setPickerDate(now); - acceptDate(now, Boolean(autoOk)); + // TODO FIX ME + setPickerDate(now as any); + acceptDate(now as any, Boolean(autoOk)); }, }), [acceptDate, autoOk, inputFormat, isOpen, now, pickerDate, setIsOpen] @@ -94,7 +87,7 @@ export function usePickerState(props: BasePickerProps) { setMobileKeyboardViewOpen(!isMobileKeyboardViewOpen); }, onDateChange: ( - newDate: MaterialUiPickersDate, + newDate: TOutput, currentVariant: WrapperVariant, isFinish: boolean | symbol = true ) => { @@ -117,7 +110,7 @@ export function usePickerState(props: BasePickerProps) { [acceptDate, autoOk, isMobileKeyboardViewOpen, pickerDate] ); - const validationError = validate(value, utils, props as any); + const validationError = validateInputValue(value, utils, props); useEffect(() => { if (onError) { onError(validationError, value); diff --git a/lib/src/_shared/hooks/useViews.tsx b/lib/src/_shared/hooks/useViews.tsx index af4a6475c04122..eaa05ab2a4abda 100644 --- a/lib/src/_shared/hooks/useViews.tsx +++ b/lib/src/_shared/hooks/useViews.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { PickerView } from '../../Picker/Picker'; import { arrayIncludes } from '../../_helpers/utils'; import { MaterialUiPickersDate } from '../../typings/date'; +import { AnyPickerView } from '../../Picker/SharedPickerProps'; export type PickerOnChangeFn = (date: MaterialUiPickersDate, isFinish?: boolean | symbol) => void; @@ -12,8 +12,8 @@ export function useViews({ isMobileKeyboardViewOpen, toggleMobileKeyboardView, }: { - views: PickerView[]; - openTo: PickerView; + views: AnyPickerView[]; + openTo: AnyPickerView; onChange: PickerOnChangeFn; isMobileKeyboardViewOpen: boolean; toggleMobileKeyboardView: () => void; diff --git a/lib/src/constants/prop-types.ts b/lib/src/constants/prop-types.ts index 3925a446f25468..261c4b8a9b01dc 100644 --- a/lib/src/constants/prop-types.ts +++ b/lib/src/constants/prop-types.ts @@ -1,5 +1,6 @@ import * as PropTypes from 'prop-types'; -import { BaseDatePickerProps } from '../DatePicker/DatePicker'; +import { MaterialUiPickersDate } from '../typings/date'; +import { DatePickerProps } from '../DatePicker/DatePicker'; import { ExportedClockViewProps } from '../views/Clock/ClockView'; const date = PropTypes.oneOfType([ @@ -11,7 +12,7 @@ const date = PropTypes.oneOfType([ const datePickerView = PropTypes.oneOf(['year', 'month', 'day']); -export type ParsableDate = object | string | number | Date | null | undefined; +export type ParsableDate = string | number | Date | null | undefined | MaterialUiPickersDate; export const DomainPropTypes = { date, datePickerView }; @@ -26,10 +27,10 @@ export const datePickerDefaultProps = { invalidDateMessage: 'Invalid Date Format', minDateMessage: 'Date should not be before minimal date', maxDateMessage: 'Date should not be after maximal date', -} as BaseDatePickerProps; +} as DatePickerProps; export const dateTimePickerDefaultProps = { ...timePickerDefaultProps, ...datePickerDefaultProps, showTabs: true, -} as ExportedClockViewProps & BaseDatePickerProps; +} as ExportedClockViewProps & DatePickerProps; diff --git a/lib/src/index.ts b/lib/src/index.ts index c801607c6b1b12..53d9eceb3e3525 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -22,7 +22,7 @@ export { Picker } from './Picker/Picker'; export { makePickerWithStateAndWrapper as makePickerWithState } from './Picker/makePickerWithState'; -export { validate } from './_helpers/text-field-helper'; +export { validateDateValue as validate } from './_helpers/text-field-helper'; export { useUtils } from './_shared/hooks/useUtils'; diff --git a/lib/src/typings/BasePicker.tsx b/lib/src/typings/BasePicker.tsx index ad95103f504b3c..853437d601f6d1 100644 --- a/lib/src/typings/BasePicker.tsx +++ b/lib/src/typings/BasePicker.tsx @@ -2,11 +2,14 @@ import { MaterialUiPickersDate } from './date'; import { ParsableDate } from '../constants/prop-types'; import { ToolbarComponentProps } from '../Picker/Picker'; -export interface BasePickerProps { +export interface BasePickerProps< + TInputValue = ParsableDate, + TDateValue = MaterialUiPickersDate | null +> { /** Picker value */ - value: ParsableDate; + value: TInputValue; /** onChange callback @DateIOType */ - onChange: (date: MaterialUiPickersDate | null, keyboardInputValue?: string) => void; + onChange: (date: TDateValue | null, keyboardInputValue?: string) => void; /** * Auto accept date on selection * @default false @@ -21,11 +24,11 @@ export interface BasePickerProps { /** Date that will be initially highlighted if null was passed */ defaultHighlight?: ParsableDate; /** Callback fired when date is accepted @DateIOType */ - onAccept?: (date: MaterialUiPickersDate) => void; + onAccept?: (date: TDateValue | null) => void; /** Callback fired when new error should be displayed * (!! This is a side effect. Be careful if you want to rerender the component) @DateIOType */ - onError?: (error: React.ReactNode, value: MaterialUiPickersDate | ParsableDate) => void; + onError?: (error: React.ReactNode, value: TInputValue | TDateValue) => void; /** On open callback */ onOpen?: () => void; /** On close callback */ @@ -44,10 +47,7 @@ export interface BasePickerProps { * Mobile picker title, displaying in the toolbar * @default "SELECT DATE" */ - toolbarTitle?: string; - /** - * Compare dates by the exact timestamp, instead of start/end of date - * @default false - */ - strictCompareDates?: boolean; + toolbarTitle?: React.ReactNode; + /** Date format, that is displaying in toolbar */ + toolbarFormat?: string; } diff --git a/lib/src/views/Calendar/CalendarView.tsx b/lib/src/views/Calendar/CalendarView.tsx index b131dd9aee0252..11253f5f707c69 100644 --- a/lib/src/views/Calendar/CalendarView.tsx +++ b/lib/src/views/Calendar/CalendarView.tsx @@ -51,6 +51,8 @@ export interface CalendarViewProps extends ExportedCalendarProps, PublicCalendar reduceAnimations?: boolean; /** Disable specific date @DateIOType */ shouldDisableDate?: (day: MaterialUiPickersDate) => boolean; + /** Callback firing on year change @DateIOType */ + onYearChange?: (date: MaterialUiPickersDate) => void; } export type ExportedCalendarViewProps = Omit< diff --git a/lib/src/wrappers/DesktopWrapper.tsx b/lib/src/wrappers/DesktopWrapper.tsx index 1c73792ff35ad7..2388c65a9616b9 100644 --- a/lib/src/wrappers/DesktopWrapper.tsx +++ b/lib/src/wrappers/DesktopWrapper.tsx @@ -46,6 +46,8 @@ export const DesktopWrapper: React.FC = ({ showTodayButton, clearable, DialogProps, + PureDateInputComponent, + KeyboardDateInputComponent = KeyboardDateInput, ...other }) => { const ref = React.useRef(); @@ -53,7 +55,7 @@ export const DesktopWrapper: React.FC = ({ return ( - + = ({ onDismiss, onSetToday, PopoverProps, + KeyboardDateInputComponent, + PureDateInputComponent = PureDateInput, ...other }) => { return ( - + > { open: boolean; onAccept: () => void; onDismiss: () => void; onClear: () => void; onSetToday: () => void; - DateInputProps: DateInputProps; + DateInputProps: TInputProps; + KeyboardDateInputComponent?: React.ComponentType; + PureDateInputComponent?: React.ComponentType; } -export type OmitInnerWrapperProps = Omit; +export type OmitInnerWrapperProps> = Omit>; export type SomeWrapper = | typeof ResponsiveWrapper diff --git a/lib/src/wrappers/makeWrapperComponent.tsx b/lib/src/wrappers/makeWrapperComponent.tsx new file mode 100644 index 00000000000000..12aab4a3fabad1 --- /dev/null +++ b/lib/src/wrappers/makeWrapperComponent.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { BasePickerProps } from '../typings/BasePicker'; +import { DateInputProps } from '../_shared/PureDateInput'; +import { ResponsiveWrapperProps } from './ResponsiveWrapper'; +import { DateValidationProps } from '../_helpers/text-field-helper'; +import { OmitInnerWrapperProps, SomeWrapper, WrapperProps } from './Wrapper'; + +interface MakePickerOptions { + PureDateInputComponent?: React.FC>; + KeyboardDateInputComponent?: React.FC>; +} + +interface WithWrapperProps { + children: React.ReactNode; + inputProps: DateInputProps; + wrapperProps: Omit; +} + +/** Creates a component that rendering modal/popover/nothing and spreading props down to text field */ +export function makeWrapperComponent( + Wrapper: TWrapper, + { KeyboardDateInputComponent, PureDateInputComponent }: MakePickerOptions +) { + function WrapperComponent( + props: Partial> & + DateValidationProps & + WithWrapperProps & + Partial> + ) { + const { + open, + value, + autoOk, + inputFormat, + minDateMessage, + maxDateMessage, + invalidDateMessage, + defaultHighlight, + onChange, + children, + clearable, + clearLabel, + DialogProps, + PopoverProps, + okLabel, + cancelLabel, + todayLabel, + inputProps, + wrapperProps, + wider, + showTabs, + onAccept, + onClose, + onOpen, + onError, + strictCompareDates, + ...restPropsForTextField + } = props; + + const WrapperComponent = Wrapper as SomeWrapper; + + return ( + + {children} + + ); + } + + return WrapperComponent; +}