From 030a8d01f76e72dafbc481874bdb4ae70917f116 Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 26 May 2023 16:03:49 +0300 Subject: [PATCH] [pickers] Add `DigitalClock` to `DesktopDateTimePicker` (#8946) --- .../date-time-picker/date-time-picker.md | 11 +- .../migration-pickers-v5.md | 4 +- .../date-pickers/date-time-picker-tabs.json | 2 +- .../date-time-picker-toolbar.json | 2 +- .../x/api/date-pickers/date-time-picker.json | 18 +- .../desktop-date-time-picker.json | 18 +- .../date-pickers/date-time-picker.json | 3 + .../desktop-date-time-picker.json | 3 + .../src/DateCalendar/DateCalendar.tsx | 4 +- .../src/DateTimePicker/DateTimePicker.tsx | 25 ++- .../DateTimePicker/DateTimePicker.types.ts | 7 +- .../src/DateTimePicker/DateTimePickerTabs.tsx | 14 +- .../DateTimePicker/DateTimePickerToolbar.tsx | 82 +++++--- .../src/DateTimePicker/shared.tsx | 24 +-- .../DesktopDateTimePicker.tsx | 76 ++++++-- .../DesktopDateTimePicker.types.ts | 23 ++- .../tests/DesktopDateTimePicker.test.tsx | 15 +- .../describes.DesktopDateTimePicker.test.tsx | 46 ++++- .../describes.DesktopTimePicker.test.tsx | 29 ++- .../tests/describes.DigitalClock.test.tsx | 5 +- .../MobileDateTimePicker.tsx | 1 + .../MobileDateTimePicker.types.ts | 31 +-- .../describes.MobileDateTimePicker.test.tsx | 39 +++- .../tests/describes.MobileTimePicker.test.tsx | 33 +++- .../MultiSectionDigitalClockSection.tsx | 10 +- ...escribes.MultiSectionDigitalClock.test.tsx | 15 +- .../StaticDateTimePicker.tsx | 1 + .../StaticDateTimePicker.types.ts | 2 +- .../tests/describes.TimeClock.test.tsx | 31 ++- .../dateTimeViewRenderers.tsx | 181 ++++++++++++++++++ .../src/dateTimeViewRenderers/index.ts | 2 + .../dateViewRenderers/dateViewRenderers.tsx | 10 +- .../DateTimeViewWrapper.tsx | 6 + .../components/DateTimeViewWrapper/index.ts | 1 + .../components/PickersToolbarButton.tsx | 5 +- .../src/internals/constants/dimensions.ts | 1 + .../hooks/usePicker/usePickerViews.ts | 23 +-- .../src/internals/hooks/useViews.tsx | 22 +-- .../src/internals/utils/date-utils.ts | 7 +- .../src/internals/utils/time-utils.ts | 6 +- .../testDayViewValidation.tsx | 114 ++++++----- .../testMinutesViewValidation.tsx | 6 +- .../testMonthViewValidation.tsx | 6 +- .../testYearViewValidation.tsx | 6 +- .../src/tests/describeValue/describeValue.tsx | 14 +- .../testControlledUnControlled.tsx | 21 +- .../describeValue/testPickerActionBar.tsx | 35 ++-- .../testPickerOpenCloseLifeCycle.tsx | 20 +- .../timeViewRenderers/timeViewRenderers.tsx | 8 +- test/utils/pickers-utils.tsx | 40 +++- 50 files changed, 820 insertions(+), 288 deletions(-) create mode 100644 packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx create mode 100644 packages/x-date-pickers/src/dateTimeViewRenderers/index.ts create mode 100644 packages/x-date-pickers/src/internals/components/DateTimeViewWrapper/DateTimeViewWrapper.tsx create mode 100644 packages/x-date-pickers/src/internals/components/DateTimeViewWrapper/index.ts diff --git a/docs/data/date-pickers/date-time-picker/date-time-picker.md b/docs/data/date-pickers/date-time-picker/date-time-picker.md index e9e59e5a37f0..bd5b39b29ab7 100644 --- a/docs/data/date-pickers/date-time-picker/date-time-picker.md +++ b/docs/data/date-pickers/date-time-picker/date-time-picker.md @@ -11,25 +11,20 @@ materialDesign: https://m2.material.io/components/date-pickers

The Date Time Picker component lets the user select a date and time.

-:::info -The component by default currently does not ship with **time** picker view experience on **desktop**. -It was a conscious decision and a first step towards having a more user friendly desktop experience [discussed in #4483](https://github.com/mui/mui-x/issues/4483). -If a desktop view experience is essential, you can revert to it by following the suggestion [in the migration guide](/x/migration/migration-pickers-v5/#stop-rendering-a-clock-on-desktop). -::: - ## Basic usage {{"demo": "BasicDateTimePicker.js"}} ## Component composition -The component is built using the `DateTimeField` for the keyboard editing, the `DateCalendar` for the date view editing and the `TimeClock` for the time view editing. -All the documented props of those three components can also be passed to the Date Time Picker component. +The component is built using the `DateTimeField` for the keyboard editing, the `DateCalendar` for the date view editing, the `DigitalClock` for the desktop view editing, and the `TimeClock` for the mobile time view editing. +All the documented props of those four components can also be passed to the Date Time Picker component. Check-out their documentation page for more information: - [Date Field](/x/react-date-pickers/date-field/) - [Date Calendar](/x/react-date-pickers/date-calendar/) +- [Digital Clock](/x/react-date-pickers/digital-clock/) - [Time Clock](/x/react-date-pickers/time-clock/) The value of the component can be uncontrolled or controlled. diff --git a/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md b/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md index 50a33d4af7b9..f47b39308f10 100644 --- a/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md +++ b/docs/data/migration/migration-pickers-v5/migration-pickers-v5.md @@ -114,8 +114,8 @@ import { DateTime } from 'luxon'; ### Stop rendering a clock on desktop In desktop mode, the `DateTimePicker` and `TimePicker` components will no longer render the [`TimeClock`](/x/react-date-pickers/time-clock/) component. -The `DateTimePicker` component currently has no replacement, but on `TimePicker` a new [`DigitalClock`](/x/react-date-pickers/digital-clock/) component has been introduced instead. -The behavior on mobile mode is still the same. +The `TimeClock` component has been replaced with a new [`DigitalClock`](/x/react-date-pickers/digital-clock/) component instead. +The behavior on `Mobile` and `Static` variants is still the same. If you were relying on Clock Picker in desktop mode for tests—make sure to check [testing caveats](/x/react-date-pickers/base-concepts/#testing-caveats) to choose the best replacement for it. You can manually re-enable the previous clock component using the new `viewRenderers` prop. diff --git a/docs/pages/x/api/date-pickers/date-time-picker-tabs.json b/docs/pages/x/api/date-pickers/date-time-picker-tabs.json index 7dd217ab815b..dd0d6cf36bc9 100644 --- a/docs/pages/x/api/date-pickers/date-time-picker-tabs.json +++ b/docs/pages/x/api/date-pickers/date-time-picker-tabs.json @@ -4,7 +4,7 @@ "view": { "type": { "name": "enum", - "description": "'day'
| 'hours'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" + "description": "'day'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" }, "required": true }, diff --git a/docs/pages/x/api/date-pickers/date-time-picker-toolbar.json b/docs/pages/x/api/date-pickers/date-time-picker-toolbar.json index 7cab1cc72b15..7828faf86303 100644 --- a/docs/pages/x/api/date-pickers/date-time-picker-toolbar.json +++ b/docs/pages/x/api/date-pickers/date-time-picker-toolbar.json @@ -4,7 +4,7 @@ "view": { "type": { "name": "enum", - "description": "'day'
| 'hours'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" + "description": "'day'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" }, "required": true }, diff --git a/docs/pages/x/api/date-pickers/date-time-picker.json b/docs/pages/x/api/date-pickers/date-time-picker.json index 01413cac9dc6..03f5ac43418b 100644 --- a/docs/pages/x/api/date-pickers/date-time-picker.json +++ b/docs/pages/x/api/date-pickers/date-time-picker.json @@ -73,7 +73,7 @@ "openTo": { "type": { "name": "enum", - "description": "'day'
| 'hours'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" + "description": "'day'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" } }, "orientation": { @@ -103,6 +103,7 @@ "shouldDisableTime": { "type": { "name": "func" } }, "shouldDisableYear": { "type": { "name": "func" } }, "showDaysOutsideCurrentMonth": { "type": { "name": "bool" } }, + "skipDisabled": { "type": { "name": "bool" } }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, "default": "{}" }, "sx": { @@ -111,17 +112,24 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timeSteps": { + "type": { + "name": "shape", + "description": "{ hours?: number, minutes?: number, seconds?: number }" + }, + "default": "{ hours: 1, minutes: 5, seconds: 5 }" + }, "value": { "type": { "name": "any" } }, "view": { "type": { "name": "enum", - "description": "'day'
| 'hours'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" + "description": "'day'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" } }, "viewRenderers": { "type": { "name": "shape", - "description": "{ day?: func, hours?: func, minutes?: func, month?: func, seconds?: func, year?: func }" + "description": "{ day?: func, hours?: func, meridiem?: func, minutes?: func, month?: func, seconds?: func, year?: func }" } }, "views": { @@ -148,6 +156,10 @@ "type": { "name": "elementType" } }, "Dialog": { "default": "PickersModalDialogRoot", "type": { "name": "elementType" } }, + "DigitalClockSectionItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + }, "Field": { "type": { "name": "elementType" } }, "InputAdornment": { "default": "InputAdornment", "type": { "name": "elementType" } }, "Layout": { "type": { "name": "elementType" } }, diff --git a/docs/pages/x/api/date-pickers/desktop-date-time-picker.json b/docs/pages/x/api/date-pickers/desktop-date-time-picker.json index 7e3b606d75a8..47d90379bde2 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-time-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-time-picker.json @@ -69,7 +69,7 @@ "openTo": { "type": { "name": "enum", - "description": "'day'
| 'hours'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" + "description": "'day'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" } }, "orientation": { @@ -99,6 +99,7 @@ "shouldDisableTime": { "type": { "name": "func" } }, "shouldDisableYear": { "type": { "name": "func" } }, "showDaysOutsideCurrentMonth": { "type": { "name": "bool" } }, + "skipDisabled": { "type": { "name": "bool" } }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, "default": "{}" }, "sx": { @@ -107,17 +108,24 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timeSteps": { + "type": { + "name": "shape", + "description": "{ hours?: number, minutes?: number, seconds?: number }" + }, + "default": "{ hours: 1, minutes: 5, seconds: 5 }" + }, "value": { "type": { "name": "any" } }, "view": { "type": { "name": "enum", - "description": "'day'
| 'hours'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" + "description": "'day'
| 'hours'
| 'meridiem'
| 'minutes'
| 'month'
| 'seconds'
| 'year'" } }, "viewRenderers": { "type": { "name": "shape", - "description": "{ day?: func, hours?: func, minutes?: func, month?: func, seconds?: func, year?: func }" + "description": "{ day?: func, hours?: func, meridiem?: func, minutes?: func, month?: func, seconds?: func, year?: func }" } }, "views": { @@ -143,6 +151,10 @@ "default": "TrapFocus from @mui/material", "type": { "name": "elementType" } }, + "DigitalClockSectionItem": { + "default": "MenuItem from '@mui/material'", + "type": { "name": "elementType" } + }, "Field": { "type": { "name": "elementType" } }, "InputAdornment": { "default": "InputAdornment", "type": { "name": "elementType" } }, "Layout": { "type": { "name": "elementType" } }, diff --git a/docs/translations/api-docs/date-pickers/date-time-picker.json b/docs/translations/api-docs/date-pickers/date-time-picker.json index 98b82dc82ad5..cc71e0dbb581 100644 --- a/docs/translations/api-docs/date-pickers/date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/date-time-picker.json @@ -55,9 +55,11 @@ "shouldDisableTime": "Disable specific time.

Signature:
function(value: TDate, view: TimeView) => boolean
value: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "shouldDisableYear": "Disable specific year.

Signature:
function(year: TDate) => boolean
year: The year to test.
returns (boolean): If true, the year will be disabled.", "showDaysOutsideCurrentMonth": "If true, days outside the current month are rendered:
- if fixedWeekNumber is defined, renders days to have the weeks requested.
- if fixedWeekNumber is not defined, renders day to fill the first and last week of the current month.
- ignored if calendars equals more than 1 on range pickers.", + "skipDisabled": "If true, disabled digital clock items will not be rendered.", "slotProps": "The props used for each component slot.", "slots": "Overridable component slots.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timeSteps": "The time steps between two time unit options. For example, if timeStep.minutes = 8, then the available minute options will be [0, 8, 16, 24, 32, 40, 48, 56]. When single column time renderer is used, only timeStep.minutes will be used.", "value": "The selected value. Used when the component is controlled.", "view": "The visible view. Used when the component view is controlled. Must be a valid option from views list.", "viewRenderers": "Define custom view renderers for each section. If null, the section will only have field editing. If undefined, internally defined view will be the used.", @@ -72,6 +74,7 @@ "DesktopTransition": "Custom component for the desktop popper Transition.", "DesktopTrapFocus": "Custom component for trapping the focus inside the views on desktop.", "Dialog": "Custom component for the dialog inside which the views are rendered on mobile.", + "DigitalClockSectionItem": "Component responsible for rendering a single multi section digital clock section item.", "Field": "Component used to enter the date with the keyboard.", "InputAdornment": "Component displayed on the start or end input adornment used to open the picker on desktop.", "Layout": "Custom component for wrapping the layout.\nIt wraps the toolbar, views, action bar, and shortcuts.", diff --git a/docs/translations/api-docs/date-pickers/desktop-date-time-picker.json b/docs/translations/api-docs/date-pickers/desktop-date-time-picker.json index 5bb756c4707f..cd92daddf64d 100644 --- a/docs/translations/api-docs/date-pickers/desktop-date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-date-time-picker.json @@ -54,9 +54,11 @@ "shouldDisableTime": "Disable specific time.

Signature:
function(value: TDate, view: TimeView) => boolean
value: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "shouldDisableYear": "Disable specific year.

Signature:
function(year: TDate) => boolean
year: The year to test.
returns (boolean): If true, the year will be disabled.", "showDaysOutsideCurrentMonth": "If true, days outside the current month are rendered:
- if fixedWeekNumber is defined, renders days to have the weeks requested.
- if fixedWeekNumber is not defined, renders day to fill the first and last week of the current month.
- ignored if calendars equals more than 1 on range pickers.", + "skipDisabled": "If true, disabled digital clock items will not be rendered.", "slotProps": "The props used for each component slot.", "slots": "Overridable component slots.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timeSteps": "The time steps between two time unit options. For example, if timeStep.minutes = 8, then the available minute options will be [0, 8, 16, 24, 32, 40, 48, 56]. When single column time renderer is used, only timeStep.minutes will be used.", "value": "The selected value. Used when the component is controlled.", "view": "The visible view. Used when the component view is controlled. Must be a valid option from views list.", "viewRenderers": "Define custom view renderers for each section. If null, the section will only have field editing. If undefined, internally defined view will be the used.", @@ -70,6 +72,7 @@ "DesktopPaper": "Custom component for the paper rendered inside the desktop picker's Popper.", "DesktopTransition": "Custom component for the desktop popper Transition.", "DesktopTrapFocus": "Custom component for trapping the focus inside the views on desktop.", + "DigitalClockSectionItem": "Component responsible for rendering a single multi section digital clock section item.", "Field": "Component used to enter the date with the keyboard.", "InputAdornment": "Component displayed on the start or end input adornment used to open the picker on desktop.", "Layout": "Custom component for wrapping the layout.\nIt wraps the toolbar, views, action bar, and shortcuts.", diff --git a/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx b/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx index 49091644462c..02c9f953cc1d 100644 --- a/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx +++ b/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx @@ -243,10 +243,10 @@ export const DateCalendar = React.forwardRef(function DateCalendar( const handleSelectedDayChange = useEventCallback((day: TDate | null) => { if (value && day) { // If there is a date already selected, then we want to keep its time - return setValueAndGoToNextView(mergeDateAndTime(utils, day, value), 'finish'); + return handleValueChange(mergeDateAndTime(utils, day, value), 'finish'); } - return setValueAndGoToNextView(day, 'finish'); + return handleValueChange(day, 'finish'); }); React.useEffect(() => { diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx index 9f50915f0c65..2ec62fb77d98 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useThemeProps } from '@mui/material/styles'; import { DesktopDateTimePicker } from '../DesktopDateTimePicker'; -import { MobileDateTimePicker } from '../MobileDateTimePicker'; +import { MobileDateTimePicker, MobileDateTimePickerProps } from '../MobileDateTimePicker'; import { DateTimePickerProps } from './DateTimePicker.types'; import { DEFAULT_DESKTOP_MODE_MEDIA_QUERY } from '../internals/utils/utils'; @@ -26,7 +26,7 @@ const DateTimePicker = React.forwardRef(function DateTimePicker( return ; } - return ; + return )} />; }) as DateTimePickerComponent; DateTimePicker.propTypes = { @@ -272,7 +272,7 @@ DateTimePicker.propTypes = { * Used when the component view is not controlled. * Must be a valid option from `views` list. */ - openTo: PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']), + openTo: PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']), /** * Force rendering in particular orientation. */ @@ -364,6 +364,11 @@ DateTimePicker.propTypes = { * @default false */ showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, disabled digital clock items will not be rendered. + * @default false + */ + skipDisabled: PropTypes.bool, /** * The props used for each component slot. * @default {} @@ -382,6 +387,17 @@ DateTimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * The time steps between two time unit options. + * For example, if `timeStep.minutes = 8`, then the available minute options will be `[0, 8, 16, 24, 32, 40, 48, 56]`. + * When single column time renderer is used, only `timeStep.minutes` will be used. + * @default{ hours: 1, minutes: 5, seconds: 5 } + */ + timeSteps: PropTypes.shape({ + hours: PropTypes.number, + minutes: PropTypes.number, + seconds: PropTypes.number, + }), /** * The selected value. * Used when the component is controlled. @@ -392,7 +408,7 @@ DateTimePicker.propTypes = { * Used when the component view is controlled. * Must be a valid option from `views` list. */ - view: PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']), + view: PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']), /** * Define custom view renderers for each section. * If `null`, the section will only have field editing. @@ -401,6 +417,7 @@ DateTimePicker.propTypes = { viewRenderers: PropTypes.shape({ day: PropTypes.func, hours: PropTypes.func, + meridiem: PropTypes.func, minutes: PropTypes.func, month: PropTypes.func, seconds: PropTypes.func, diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts index cf9e900c257f..3ed5b62e344d 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.types.ts @@ -3,6 +3,7 @@ import { DesktopDateTimePickerSlotsComponent, DesktopDateTimePickerSlotsComponentsProps, } from '../DesktopDateTimePicker'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; import { MobileDateTimePickerProps, @@ -12,15 +13,15 @@ import { export interface DateTimePickerSlotsComponents extends DesktopDateTimePickerSlotsComponent, - MobileDateTimePickerSlotsComponent {} + MobileDateTimePickerSlotsComponent {} export interface DateTimePickerSlotsComponentsProps extends DesktopDateTimePickerSlotsComponentsProps, - MobileDateTimePickerSlotsComponentsProps {} + MobileDateTimePickerSlotsComponentsProps {} export interface DateTimePickerProps extends DesktopDateTimePickerProps, - MobileDateTimePickerProps { + Omit, 'views'> { /** * CSS media query when `Mobile` mode will be changed to `Desktop`. * @default '@media (pointer: fine)' diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerTabs.tsx b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerTabs.tsx index c04807a2e7c9..36628651d59a 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerTabs.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerTabs.tsx @@ -5,25 +5,26 @@ import Tabs, { tabsClasses } from '@mui/material/Tabs'; import { styled, useThemeProps } from '@mui/material/styles'; import { unstable_composeClasses as composeClasses } from '@mui/utils'; import { Time, DateRange } from '../internals/components/icons'; -import { DateOrTimeView } from '../models'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; import { useLocaleText } from '../internals/hooks/useUtils'; import { DateTimePickerTabsClasses, getDateTimePickerTabsUtilityClass, } from './dateTimePickerTabsClasses'; import { BaseTabsProps, ExportedBaseTabsProps } from '../internals/models/props/tabs'; +import { isDatePickerView } from '../internals/utils/date-utils'; type TabValue = 'date' | 'time'; -const viewToTab = (view: DateOrTimeView): TabValue => { - if (['day', 'month', 'year'].includes(view)) { +const viewToTab = (view: DateOrTimeViewWithMeridiem): TabValue => { + if (isDatePickerView(view)) { return 'date'; } return 'time'; }; -const tabToView = (tab: TabValue): DateOrTimeView => { +const tabToView = (tab: TabValue): DateOrTimeViewWithMeridiem => { if (tab === 'date') { return 'day'; } @@ -51,7 +52,7 @@ export interface ExportedDateTimePickerTabsProps extends ExportedBaseTabsProps { export interface DateTimePickerTabsProps extends ExportedDateTimePickerTabsProps, - BaseTabsProps { + BaseTabsProps { /** * Override or extend the styles applied to the component. */ @@ -158,7 +159,8 @@ DateTimePickerTabs.propTypes = { /** * Currently visible picker view. */ - view: PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']).isRequired, + view: PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']) + .isRequired, } as any; export { DateTimePickerTabs }; diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx index d720dcd43f81..83bfd879d007 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePickerToolbar.tsx @@ -13,8 +13,9 @@ import { DateTimePickerToolbarClasses, getDateTimePickerToolbarUtilityClass, } from './dateTimePickerToolbarClasses'; -import { DateOrTimeView } from '../models'; +import { DateOrTimeViewWithMeridiem, WrapperVariant } from '../internals/models'; import { useMeridiemMode } from '../internals/hooks/date-helpers-hooks'; +import { MULTI_SECTION_CLOCK_SECTION_WIDTH } from '../internals/constants/dimensions'; export interface ExportedDateTimePickerToolbarProps extends ExportedBaseToolbarProps { ampm?: boolean; @@ -23,11 +24,12 @@ export interface ExportedDateTimePickerToolbarProps extends ExportedBaseToolbarP export interface DateTimePickerToolbarProps extends ExportedDateTimePickerToolbarProps, - BaseToolbarProps { + BaseToolbarProps { /** * Override or extend the styles applied to the component. */ classes?: Partial; + toolbarVariant?: WrapperVariant; } const useUtilityClasses = (ownerState: DateTimePickerToolbarProps & { theme: Theme }) => { @@ -49,9 +51,17 @@ const DateTimePickerToolbarRoot = styled(PickersToolbar, { name: 'MuiDateTimePickerToolbar', slot: 'Root', overridesResolver: (props, styles) => styles.root, -})<{ ownerState: DateTimePickerToolbarProps }>(({ theme }) => ({ - paddingLeft: 16, - paddingRight: 16, +})<{ ownerState: DateTimePickerToolbarProps }>(({ theme, ownerState }) => ({ + paddingLeft: ownerState.toolbarVariant === 'desktop' && !ownerState.isLandscape ? 24 : 16, + paddingRight: ownerState.toolbarVariant === 'desktop' && !ownerState.isLandscape ? 0 : 16, + borderBottom: + ownerState.toolbarVariant === 'desktop' + ? `1px solid ${(theme.vars || theme).palette.divider}` + : undefined, + borderRight: + ownerState.toolbarVariant === 'desktop' && ownerState.isLandscape + ? `1px solid ${(theme.vars || theme).palette.divider}` + : undefined, justifyContent: 'space-around', position: 'relative', [`& .${pickersToolbarClasses.penIconButton}`]: { @@ -98,10 +108,18 @@ const DateTimePickerToolbarTimeContainer = styled('div', { slot: 'TimeContainer', overridesResolver: (props, styles) => styles.timeContainer, })<{ ownerState: DateTimePickerToolbarProps }>(({ theme, ownerState }) => { - const direction = ownerState.isLandscape ? 'column' : 'row'; + const direction = + ownerState.isLandscape && ownerState.toolbarVariant !== 'desktop' ? 'column' : 'row'; return { display: 'flex', flexDirection: direction, + ...(ownerState.toolbarVariant === 'desktop' && { + ...(!ownerState.isLandscape && { + gap: 9, + marginRight: 4, + alignSelf: 'flex-end', + }), + }), ...(theme.direction === 'rtl' && { flexDirection: `${direction}-reverse`, }), @@ -112,8 +130,9 @@ const DateTimePickerToolbarTimeDigitsContainer = styled('div', { name: 'MuiDateTimePickerToolbar', slot: 'TimeDigitsContainer', overridesResolver: (props, styles) => styles.timeDigitsContainer, -})(({ theme }) => ({ +})<{ ownerState: DateTimePickerToolbarProps }>(({ theme, ownerState }) => ({ display: 'flex', + ...(ownerState.toolbarVariant === 'desktop' && { gap: 1.5 }), ...(theme.direction === 'rtl' && { flexDirection: 'row-reverse', }), @@ -139,10 +158,10 @@ const DateTimePickerToolbarSeparator = styled(PickersToolbarText, { overridesResolver: (props, styles) => styles.separator, })<{ ownerState: DateTimePickerToolbarProps; -}>({ - margin: '0 4px 0 2px', +}>(({ ownerState }) => ({ + margin: ownerState.toolbarVariant === 'desktop' ? 0 : '0 4px 0 2px', cursor: 'default', -}); +})); // Taken from TimePickerToolbar const DateTimePickerToolbarAmPmSelection = styled('div', { @@ -186,6 +205,7 @@ function DateTimePickerToolbar(inProps: DateTimePickerToo views, disabled, readOnly, + toolbarVariant = 'mobile', ...other } = props; const ownerState = props; @@ -193,6 +213,7 @@ function DateTimePickerToolbar(inProps: DateTimePickerToo const { meridiemMode, handleMeridiemChange } = useMeridiemMode(value, ampm, onChange); const showAmPmControl = Boolean(ampm && !ampmInClock); + const isDesktop = toolbarVariant === 'desktop'; const localeText = useLocaleText(); const theme = useTheme(); @@ -236,7 +257,7 @@ function DateTimePickerToolbar(inProps: DateTimePickerToo {views.includes('day') && ( onViewChange('day')} selected={view === 'day'} @@ -245,10 +266,14 @@ function DateTimePickerToolbar(inProps: DateTimePickerToo )} - + {views.includes('hours') && ( onViewChange('hours')} selected={view === 'hours'} @@ -259,13 +284,14 @@ function DateTimePickerToolbar(inProps: DateTimePickerToo {views.includes('minutes') && ( onViewChange('minutes')} selected={view === 'minutes'} @@ -277,13 +303,14 @@ function DateTimePickerToolbar(inProps: DateTimePickerToo {views.includes('seconds') && ( onViewChange('seconds')} selected={view === 'seconds'} @@ -292,13 +319,12 @@ function DateTimePickerToolbar(inProps: DateTimePickerToo )} - {showAmPmControl && ( + {showAmPmControl && !isDesktop && ( (inProps: DateTimePickerToo disabled={disabled} /> (inProps: DateTimePickerToo /> )} + + {ampm && isDesktop && ( + onViewChange('meridiem')} + selected={view === 'meridiem'} + value={value && meridiemMode ? utils.getMeridiemText(meridiemMode) : '--'} + width={MULTI_SECTION_CLOCK_SECTION_WIDTH} + /> + )} ); @@ -362,13 +398,15 @@ DateTimePickerToolbar.propTypes = { * @default "––" */ toolbarPlaceholder: PropTypes.node, + toolbarVariant: PropTypes.oneOf(['desktop', 'mobile']), value: PropTypes.any, /** * Currently visible picker view. */ - view: PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']).isRequired, + view: PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']) + .isRequired, views: PropTypes.arrayOf( - PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']).isRequired, + PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']).isRequired, ).isRequired, } as any; diff --git a/packages/x-date-pickers/src/DateTimePicker/shared.tsx b/packages/x-date-pickers/src/DateTimePicker/shared.tsx index 970fac3ced05..729cf69aba5f 100644 --- a/packages/x-date-pickers/src/DateTimePicker/shared.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/shared.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useThemeProps } from '@mui/material/styles'; import { DefaultizedProps } from '../internals/models/helpers'; -import { DateOrTimeView, DateTimeValidationError } from '../models'; +import { DateTimeValidationError } from '../models'; import { useDefaultDates, useUtils } from '../internals/hooks/useUtils'; import { DateCalendarSlotsComponent, @@ -36,7 +36,7 @@ import { TimeViewRendererProps } from '../timeViewRenderers'; import { applyDefaultViewProps } from '../internals/utils/views'; import { uncapitalizeObjectKeys, UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; import { BaseClockProps, ExportedBaseClockProps } from '../internals/models/props/clock'; -import { TimeViewWithMeridiem } from '../internals/models'; +import { DateOrTimeViewWithMeridiem, TimeViewWithMeridiem } from '../internals/models'; export interface BaseDateTimePickerSlotsComponent extends DateCalendarSlotsComponent, @@ -66,8 +66,8 @@ export interface BaseDateTimePickerSlotsComponentsProps toolbar?: ExportedDateTimePickerToolbarProps; } -export interface BaseDateTimePickerProps - extends BasePickerInputProps, +export interface BaseDateTimePickerProps + extends BasePickerInputProps, Omit, 'onViewChange'>, ExportedBaseClockProps, DateTimeValidationProps { @@ -106,8 +106,8 @@ export interface BaseDateTimePickerProps viewRenderers?: Partial< PickerViewRendererLookup< TDate | null, - DateOrTimeView, - DateViewRendererProps & + TView, + DateViewRendererProps & TimeViewRendererProps>, {} > @@ -116,7 +116,8 @@ export interface BaseDateTimePickerProps type UseDateTimePickerDefaultizedProps< TDate, - Props extends BaseDateTimePickerProps, + TView extends DateOrTimeViewWithMeridiem, + Props extends BaseDateTimePickerProps, > = LocalizedComponent< TDate, DefaultizedProps< @@ -132,11 +133,12 @@ type UseDateTimePickerDefaultizedProps< export function useDateTimePickerDefaultizedProps< TDate, - Props extends BaseDateTimePickerProps, + TView extends DateOrTimeViewWithMeridiem, + Props extends BaseDateTimePickerProps, >( props: Props, name: string, -): Omit, 'components' | 'componentsProps'> { +): Omit, 'components' | 'componentsProps'> { const utils = useUtils(); const defaultDates = useDefaultDates(); const themeProps = useThemeProps({ @@ -164,8 +166,8 @@ export function useDateTimePickerDefaultizedProps< ...applyDefaultViewProps({ views: themeProps.views, openTo: themeProps.openTo, - defaultViews: ['year', 'day', 'hours', 'minutes'], - defaultOpenTo: 'day', + defaultViews: ['year', 'day', 'hours', 'minutes'] as TView[], + defaultOpenTo: 'day' as TView, }), ampm, localeText, diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx index 4693be0916dc..e067f964db91 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx @@ -5,13 +5,15 @@ import { singleItemValueManager } from '../internals/utils/valueManagers'; import { DateTimeField } from '../DateTimeField'; import { DesktopDateTimePickerProps } from './DesktopDateTimePicker.types'; import { useDateTimePickerDefaultizedProps } from '../DateTimePicker/shared'; -import { renderDateViewCalendar } from '../dateViewRenderers'; +import { renderDateViewCalendar } from '../dateViewRenderers/dateViewRenderers'; +import { renderDesktopDateTimeView } from '../dateTimeViewRenderers'; import { useLocaleText, validateDateTime } from '../internals'; -import { DateOrTimeView } from '../models'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; import { Calendar } from '../internals/components/icons'; import { useDesktopPicker } from '../internals/hooks/useDesktopPicker'; import { extractValidationProps } from '../internals/utils/validation/extractValidationProps'; import { PickerViewRendererLookup } from '../internals/hooks/usePicker/usePickerViews'; +import { PickersActionBarAction } from '../PickersActionBar'; type DesktopDateTimePickerComponent = (( props: DesktopDateTimePickerProps & React.RefAttributes, @@ -26,26 +28,50 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker >(inProps, 'MuiDesktopDateTimePicker'); - const viewRenderers: PickerViewRendererLookup = { - day: renderDateViewCalendar, - month: renderDateViewCalendar, - year: renderDateViewCalendar, - hours: null, - minutes: null, - seconds: null, - ...defaultizedProps.viewRenderers, - }; + const timeSteps = { hours: 1, minutes: 5, seconds: 5, ...defaultizedProps.timeSteps }; + const shouldUseNewRenderer = + !defaultizedProps.viewRenderers || Object.keys(defaultizedProps.viewRenderers).length === 0; + + const viewRenderers: PickerViewRendererLookup = + // we can only ensure the expected two-column layout if none of the renderers are overridden + shouldUseNewRenderer + ? { + day: renderDesktopDateTimeView, + month: renderDesktopDateTimeView, + year: renderDesktopDateTimeView, + hours: renderDesktopDateTimeView, + minutes: renderDesktopDateTimeView, + seconds: renderDesktopDateTimeView, + meridiem: renderDesktopDateTimeView, + } + : { + day: renderDateViewCalendar, + month: renderDateViewCalendar, + year: renderDateViewCalendar, + hours: null, + minutes: null, + seconds: null, + meridiem: null, + ...defaultizedProps.viewRenderers, + }; const ampmInClock = defaultizedProps.ampmInClock ?? true; + // add "accept" action only when the new date time view renderers are used + const actionBarActions: PickersActionBarAction[] = shouldUseNewRenderer ? ['accept'] : []; // Props with the default values specific to the desktop variant const props = { ...defaultizedProps, viewRenderers, + views: (defaultizedProps.ampm + ? [...defaultizedProps.views, 'meridiem'] + : defaultizedProps.views) as DateOrTimeViewWithMeridiem[], yearsPerRow: defaultizedProps.yearsPerRow ?? 4, ampmInClock, + timeSteps, slots: { field: DateTimeField, openPickerIcon: Calendar, @@ -61,16 +87,21 @@ const DesktopDateTimePicker = React.forwardRef(function DesktopDateTimePicker({ + const { renderPicker } = useDesktopPicker({ props, valueManager: singleItemValueManager, valueType: 'date-time', @@ -318,7 +349,7 @@ DesktopDateTimePicker.propTypes = { * Used when the component view is not controlled. * Must be a valid option from `views` list. */ - openTo: PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']), + openTo: PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']), /** * Force rendering in particular orientation. */ @@ -410,6 +441,11 @@ DesktopDateTimePicker.propTypes = { * @default false */ showDaysOutsideCurrentMonth: PropTypes.bool, + /** + * If `true`, disabled digital clock items will not be rendered. + * @default false + */ + skipDisabled: PropTypes.bool, /** * The props used for each component slot. * @default {} @@ -428,6 +464,17 @@ DesktopDateTimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * The time steps between two time unit options. + * For example, if `timeStep.minutes = 8`, then the available minute options will be `[0, 8, 16, 24, 32, 40, 48, 56]`. + * When single column time renderer is used, only `timeStep.minutes` will be used. + * @default{ hours: 1, minutes: 5, seconds: 5 } + */ + timeSteps: PropTypes.shape({ + hours: PropTypes.number, + minutes: PropTypes.number, + seconds: PropTypes.number, + }), /** * The selected value. * Used when the component is controlled. @@ -438,7 +485,7 @@ DesktopDateTimePicker.propTypes = { * Used when the component view is controlled. * Must be a valid option from `views` list. */ - view: PropTypes.oneOf(['day', 'hours', 'minutes', 'month', 'seconds', 'year']), + view: PropTypes.oneOf(['day', 'hours', 'meridiem', 'minutes', 'month', 'seconds', 'year']), /** * Define custom view renderers for each section. * If `null`, the section will only have field editing. @@ -447,6 +494,7 @@ DesktopDateTimePicker.propTypes = { viewRenderers: PropTypes.shape({ day: PropTypes.func, hours: PropTypes.func, + meridiem: PropTypes.func, minutes: PropTypes.func, month: PropTypes.func, seconds: PropTypes.func, diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts index 45c68f08a00d..04795749a5f7 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.types.ts @@ -11,21 +11,34 @@ import { import { MakeOptional } from '../internals/models/helpers'; import { DateOrTimeView } from '../models'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { DesktopOnlyTimePickerProps } from '../internals/models/props/clock'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; +import { + MultiSectionDigitalClockSlotsComponent, + MultiSectionDigitalClockSlotsComponentsProps, +} from '../MultiSectionDigitalClock'; export interface DesktopDateTimePickerSlotsComponent extends BaseDateTimePickerSlotsComponent, MakeOptional< - UseDesktopPickerSlotsComponent, + UseDesktopPickerSlotsComponent, 'Field' | 'OpenPickerIcon' - > {} + >, + MultiSectionDigitalClockSlotsComponent {} export interface DesktopDateTimePickerSlotsComponentsProps extends BaseDateTimePickerSlotsComponentsProps, - ExportedUseDesktopPickerSlotsComponentsProps {} + ExportedUseDesktopPickerSlotsComponentsProps, + MultiSectionDigitalClockSlotsComponentsProps {} export interface DesktopDateTimePickerProps - extends BaseDateTimePickerProps, - DesktopOnlyPickerProps { + extends BaseDateTimePickerProps, + DesktopOnlyPickerProps, + Omit, 'thresholdToRenderTimeInASingleColumn'> { + /** + * Available views. + */ + views?: readonly DateOrTimeView[]; /** * Years rendered per row. * @default 4 diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx index a93e76a74a5a..aac8ac0dfd51 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/DesktopDateTimePicker.test.tsx @@ -33,7 +33,7 @@ describe('', () => { onChange={onChange} onAccept={onAccept} onClose={onClose} - defaultValue={adapterToUse.date(new Date(2018, 0, 1, 11, 53))} + defaultValue={adapterToUse.date(new Date(2018, 0, 1, 11, 55))} openTo="year" />, ); @@ -43,14 +43,23 @@ describe('', () => { // Select year userEvent.mousePress(screen.getByRole('button', { name: '2025' })); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2025, 0, 1, 11, 53)); + expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2025, 0, 1, 11, 55)); expect(onAccept.callCount).to.equal(0); expect(onClose.callCount).to.equal(0); // Change the date (same value) userEvent.mousePress(screen.getByRole('gridcell', { name: '1' })); expect(onChange.callCount).to.equal(1); // Don't call onChange again since the value did not change - expect(onAccept.callCount).to.equal(0); + // Change the hours (same value) + userEvent.mousePress(screen.getByRole('option', { name: '11 hours' })); + expect(onChange.callCount).to.equal(1); // Don't call onChange again since the value did not change + // Change the minutes (same value) + userEvent.mousePress(screen.getByRole('option', { name: '55 minutes' })); + expect(onChange.callCount).to.equal(1); // Don't call onChange again since the value did not change + // Change the meridiem (same value) + userEvent.mousePress(screen.getByRole('option', { name: 'AM' })); + expect(onChange.callCount).to.equal(1); // Don't call onChange again since the value did not change + expect(onAccept.callCount).to.equal(1); expect(onClose.callCount).to.equal(1); }); }); diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx index 94141841a484..0b009f7be51e 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/tests/describes.DesktopDateTimePicker.test.tsx @@ -29,11 +29,10 @@ describe(' - Describes', () => { componentFamily: 'picker', type: 'date-time', variant: 'desktop', - defaultProps: { - views: ['day'], - openTo: 'day', - }, - values: [adapterToUse.date(new Date(2018, 0, 1)), adapterToUse.date(new Date(2018, 0, 2))], + values: [ + adapterToUse.date(new Date(2018, 0, 1, 11, 30)), + adapterToUse.date(new Date(2018, 0, 2, 12, 35)), + ], emptyValue: null, clock, assertRenderedValue: (expectedValue: any) => { @@ -52,16 +51,51 @@ describe(' - Describes', () => { expectInputValue(input, expectedValueStr); }, setNewValue: (value, { isOpened, applySameValue, selectSection }) => { - const newValue = applySameValue ? value : adapterToUse.addDays(value, 1); + const newValue = applySameValue + ? value + : adapterToUse.addMinutes(adapterToUse.addHours(adapterToUse.addDays(value, 1), 1), 5); if (isOpened) { userEvent.mousePress( screen.getByRole('gridcell', { name: adapterToUse.getDate(newValue).toString() }), ); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + const hours = adapterToUse.format(newValue, hasMeridiem ? 'hours12h' : 'hours24h'); + const hoursNumber = adapterToUse.getHours(newValue); + userEvent.mousePress(screen.getByRole('option', { name: `${parseInt(hours, 10)} hours` })); + userEvent.mousePress( + screen.getByRole('option', { name: `${adapterToUse.getMinutes(newValue)} minutes` }), + ); + if (hasMeridiem) { + // meridiem is an extra view on `DesktopDateTimePicker` + // we need to click it to finish selection + userEvent.mousePress( + screen.getByRole('option', { name: hoursNumber >= 12 ? 'PM' : 'AM' }), + ); + } } else { selectSection('day'); const input = getTextbox(); userEvent.keyPress(input, { key: 'ArrowUp' }); + // move to the hours section + userEvent.keyPress(input, { key: 'ArrowRight' }); + userEvent.keyPress(input, { key: 'ArrowRight' }); + userEvent.keyPress(input, { key: 'ArrowUp' }); + // move to the minutes section + userEvent.keyPress(input, { key: 'ArrowRight' }); + // increment by 5 minutes + userEvent.keyPress(input, { key: 'PageUp' }); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + if (hasMeridiem) { + // move to the meridiem section + userEvent.keyPress(input, { key: 'ArrowRight' }); + const previousHours = adapterToUse.getHours(value); + const newHours = adapterToUse.getHours(newValue); + // update meridiem section if it changed + if ((previousHours < 12 && newHours >= 12) || (previousHours >= 12 && newHours < 12)) { + userEvent.keyPress(input, { key: 'ArrowUp' }); + } + } } return newValue; diff --git a/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx b/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx index 265c5be72fbf..77cca31430e5 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/tests/describes.DesktopTimePicker.test.tsx @@ -54,12 +54,9 @@ describe(' - Describes', () => { componentFamily: 'picker', type: 'time', variant: 'desktop', - defaultProps: { - views: ['hours'], - }, values: [ - adapterToUse.date(new Date(2018, 0, 1, 15, 30)), - adapterToUse.date(new Date(2018, 0, 1, 18, 30)), + adapterToUse.date(new Date(2018, 0, 1, 11, 30)), + adapterToUse.date(new Date(2018, 0, 1, 12, 35)), ], emptyValue: null, clock, @@ -77,13 +74,18 @@ describe(' - Describes', () => { ); }, setNewValue: (value, { isOpened, applySameValue, selectSection }) => { - const newValue = applySameValue ? value : adapterToUse.addHours(value, 1); + const newValue = applySameValue + ? value + : adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 5); if (isOpened) { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); const hours = adapterToUse.format(newValue, hasMeridiem ? 'hours12h' : 'hours24h'); const hoursNumber = adapterToUse.getHours(newValue); userEvent.mousePress(screen.getByRole('option', { name: `${parseInt(hours, 10)} hours` })); + userEvent.mousePress( + screen.getByRole('option', { name: `${adapterToUse.getMinutes(newValue)} minutes` }), + ); if (hasMeridiem) { // meridiem is an extra view on `DesktopTimePicker` // we need to click it to finish selection @@ -95,6 +97,21 @@ describe(' - Describes', () => { selectSection('hours'); const input = getTextbox(); userEvent.keyPress(input, { key: 'ArrowUp' }); + // move to the minutes section + userEvent.keyPress(input, { key: 'ArrowRight' }); + // increment by 5 minutes + userEvent.keyPress(input, { key: 'PageUp' }); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + if (hasMeridiem) { + // move to the meridiem section + userEvent.keyPress(input, { key: 'ArrowRight' }); + const previousHours = adapterToUse.getHours(value); + const newHours = adapterToUse.getHours(newValue); + // update meridiem section if it changed + if ((previousHours < 12 && newHours >= 12) || (previousHours >= 12 && newHours < 12)) { + userEvent.keyPress(input, { key: 'ArrowUp' }); + } + } } return newValue; diff --git a/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx b/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx index b3c358217efb..ecc047d8aea4 100644 --- a/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx +++ b/packages/x-date-pickers/src/DigitalClock/tests/describes.DigitalClock.test.tsx @@ -62,14 +62,15 @@ describe(' - Describes', () => { } }, setNewValue: (value) => { + const newValue = adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 30); const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); const formattedLabel = adapterToUse.format( - value, + newValue, hasMeridiem ? 'fullTime12h' : 'fullTime24h', ); userEvent.mousePress(screen.getByRole('option', { name: formattedLabel })); - return value; + return newValue; }, })); }); diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx index 3d4cea70370c..490dd5369d7e 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx @@ -25,6 +25,7 @@ const MobileDateTimePicker = React.forwardRef(function MobileDateTimePicker >(inProps, 'MuiMobileDateTimePicker'); diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts index 29e4e02019f7..61be2967f7a7 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.types.ts @@ -11,38 +11,45 @@ import { import { MakeOptional } from '../internals/models/helpers'; import { DateOrTimeView } from '../models'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; -export interface MobileDateTimePickerSlotsComponent - extends BaseDateTimePickerSlotsComponent, - MakeOptional, 'Field'> {} +export interface MobileDateTimePickerSlotsComponent< + TDate, + TView extends DateOrTimeViewWithMeridiem = DateOrTimeView, +> extends BaseDateTimePickerSlotsComponent, + MakeOptional, 'Field'> {} -export interface MobileDateTimePickerSlotsComponentsProps - extends BaseDateTimePickerSlotsComponentsProps, - ExportedUseMobilePickerSlotsComponentsProps {} +export interface MobileDateTimePickerSlotsComponentsProps< + TDate, + TView extends DateOrTimeViewWithMeridiem = DateOrTimeView, +> extends BaseDateTimePickerSlotsComponentsProps, + ExportedUseMobilePickerSlotsComponentsProps {} -export interface MobileDateTimePickerProps - extends BaseDateTimePickerProps, +export interface MobileDateTimePickerProps< + TDate, + TView extends DateOrTimeViewWithMeridiem = DateOrTimeView, +> extends BaseDateTimePickerProps, MobileOnlyPickerProps { /** * Overridable components. * @default {} * @deprecated Please use `slots`. */ - components?: MobileDateTimePickerSlotsComponent; + components?: MobileDateTimePickerSlotsComponent; /** * The props used for each component slot. * @default {} * @deprecated Please use `slotProps`. */ - componentsProps?: MobileDateTimePickerSlotsComponentsProps; + componentsProps?: MobileDateTimePickerSlotsComponentsProps; /** * Overridable component slots. * @default {} */ - slots?: UncapitalizeObjectKeys>; + slots?: UncapitalizeObjectKeys>; /** * The props used for each component slot. * @default {} */ - slotProps?: MobileDateTimePickerSlotsComponentsProps; + slotProps?: MobileDateTimePickerSlotsComponentsProps; } diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx index f2a14eb63407..53aa9d381a21 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/tests/describes.MobileDateTimePicker.test.tsx @@ -34,11 +34,11 @@ describe(' - Describes', () => { componentFamily: 'picker', type: 'date-time', variant: 'mobile', - defaultProps: { - openTo: 'minutes', - }, clock, - values: [adapterToUse.date(new Date(2018, 0, 1)), adapterToUse.date(new Date(2018, 0, 2))], + values: [ + adapterToUse.date(new Date(2018, 0, 1, 11, 30)), + adapterToUse.date(new Date(2018, 0, 2, 12, 35)), + ], emptyValue: null, assertRenderedValue: (expectedValue: any) => { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); @@ -57,18 +57,41 @@ describe(' - Describes', () => { }, setNewValue: (value, { isOpened, applySameValue }) => { if (!isOpened) { - openPicker({ type: 'time', variant: 'mobile' }); + openPicker({ type: 'date-time', variant: 'mobile' }); } - const newValue = applySameValue ? value : adapterToUse.addMinutes(value, 1); - const hourClockEvent = getClockTouchEvent(adapterToUse.getMinutes(newValue), 'minutes'); + const newValue = applySameValue + ? value + : adapterToUse.addMinutes(adapterToUse.addHours(adapterToUse.addDays(value, 1), 1), 5); + userEvent.mousePress( + screen.getByRole('gridcell', { name: adapterToUse.getDate(newValue).toString() }), + ); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + // change hours + const hourClockEvent = getClockTouchEvent( + adapterToUse.getHours(newValue), + hasMeridiem ? '12hours' : '24hours', + ); fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', hourClockEvent); fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', hourClockEvent); + // change minutes + const minutesClockEvent = getClockTouchEvent(adapterToUse.getMinutes(newValue), 'minutes'); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', minutesClockEvent); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', minutesClockEvent); + + if (hasMeridiem) { + const newHours = adapterToUse.getHours(newValue); + // select appropriate meridiem + userEvent.mousePress(screen.getByRole('button', { name: newHours >= 12 ? 'PM' : 'AM' })); + } - // Close the picker to return to the initial state + // Close the picker if (!isOpened) { userEvent.keyPress(document.activeElement!, { key: 'Escape' }); clock.runToLast(); + } else { + // return to the date view in case we'd like to repeat the selection process + userEvent.mousePress(screen.getByRole('tab', { name: 'pick date' })); } return newValue; diff --git a/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx b/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx index f9b385141ece..56b4090585ce 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/tests/describes.MobileTimePicker.test.tsx @@ -61,13 +61,10 @@ describe(' - Describes', () => { type: 'time', variant: 'mobile', values: [ - adapterToUse.date(new Date(2018, 0, 1, 15, 30)), - adapterToUse.date(new Date(2018, 0, 1, 18, 30)), + adapterToUse.date(new Date(2018, 0, 1, 11, 30)), + adapterToUse.date(new Date(2018, 0, 1, 12, 35)), ], emptyValue: null, - defaultProps: { - openTo: 'minutes', - }, clock, assertRenderedValue: (expectedValue: any) => { const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); @@ -86,15 +83,35 @@ describe(' - Describes', () => { openPicker({ type: 'time', variant: 'mobile' }); } - const newValue = applySameValue ? value : adapterToUse.addMinutes(value, 1); - const hourClockEvent = getClockTouchEvent(adapterToUse.getMinutes(newValue), 'minutes'); + const newValue = applySameValue + ? value + : adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 5); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + // change hours + const hourClockEvent = getClockTouchEvent( + adapterToUse.getHours(newValue), + hasMeridiem ? '12hours' : '24hours', + ); fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', hourClockEvent); fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', hourClockEvent); + // change minutes + const minutesClockEvent = getClockTouchEvent(adapterToUse.getMinutes(newValue), 'minutes'); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', minutesClockEvent); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', minutesClockEvent); + + if (hasMeridiem) { + const newHours = adapterToUse.getHours(newValue); + // select appropriate meridiem + userEvent.mousePress(screen.getByRole('button', { name: newHours >= 12 ? 'PM' : 'AM' })); + } - // Close the picker to return to the initial state + // Close the picker if (!isOpened) { userEvent.keyPress(document.activeElement!, { key: 'Escape' }); clock.runToLast(); + } else { + // return to the hours view in case we'd like to repeat the selection process + userEvent.mousePress(screen.getByRole('button', { name: 'open previous view' })); } return newValue; diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx index f6b032929f39..7d52f4466f9a 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClockSection.tsx @@ -15,7 +15,10 @@ import type { MultiSectionDigitalClockSlotsComponentsProps, } from './MultiSectionDigitalClock.types'; import { UncapitalizeObjectKeys } from '../internals/utils/slots-migration'; -import { DIGITAL_CLOCK_VIEW_HEIGHT } from '../internals/constants/dimensions'; +import { + DIGITAL_CLOCK_VIEW_HEIGHT, + MULTI_SECTION_CLOCK_SECTION_WIDTH, +} from '../internals/constants/dimensions'; export interface ExportedMultiSectionDigitalClockSectionProps { className?: string; @@ -66,7 +69,8 @@ const MultiSectionDigitalClockSectionRoot = styled(MenuList, { '&:after': { display: 'block', content: '""', - height: 188, + // subtracting the height of one item, extra margin and borders to make sure the max height is correct + height: 'calc(100% - 40px - 6px)', }, }), ); @@ -78,7 +82,7 @@ const MultiSectionDigitalClockSectionItem = styled(MenuItem, { })(({ theme }) => ({ padding: 8, margin: '2px 4px', - width: 48, + width: MULTI_SECTION_CLOCK_SECTION_WIDTH, justifyContent: 'center', '&:first-of-type': { marginTop: 4, diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx index 02cb357fb8cb..205470e0d858 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/describes.MultiSectionDigitalClock.test.tsx @@ -42,8 +42,8 @@ describe(' - Describes', () => { type: 'time', variant: 'desktop', values: [ - adapterToUse.date(new Date(2018, 0, 1, 15, 30)), - adapterToUse.date(new Date(2018, 0, 1, 16, 15)), + adapterToUse.date(new Date(2018, 0, 1, 11, 30)), + adapterToUse.date(new Date(2018, 0, 1, 12, 35)), ], emptyValue: null, clock, @@ -62,29 +62,30 @@ describe(' - Describes', () => { expect(selectedItems[1]).to.have.text(minutesLabel); if (hasMeridiem) { expect(selectedItems[2]).to.have.text( - adapterToUse.getMeridiemText(adapterToUse.getHours(expectedValue) > 12 ? 'pm' : 'am'), + adapterToUse.getMeridiemText(adapterToUse.getHours(expectedValue) >= 12 ? 'pm' : 'am'), ); } } }, setNewValue: (value) => { + const newValue = adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 5); const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); const hoursLabel = parseInt( - adapterToUse.format(value, hasMeridiem ? 'hours12h' : 'hours24h'), + adapterToUse.format(newValue, hasMeridiem ? 'hours12h' : 'hours24h'), 10, ); - const minutesLabel = adapterToUse.getMinutes(value).toString(); + const minutesLabel = adapterToUse.getMinutes(newValue).toString(); userEvent.mousePress(screen.getByRole('option', { name: `${hoursLabel} hours` })); userEvent.mousePress(screen.getByRole('option', { name: `${minutesLabel} minutes` })); if (hasMeridiem) { userEvent.mousePress( screen.getByRole('option', { - name: adapterToUse.getMeridiemText(adapterToUse.getHours(value) > 12 ? 'pm' : 'am'), + name: adapterToUse.getMeridiemText(adapterToUse.getHours(newValue) >= 12 ? 'pm' : 'am'), }), ); } - return value; + return newValue; }, })); }); diff --git a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx index 22bb0b0cd4b0..439865c30d8d 100644 --- a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx +++ b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx @@ -20,6 +20,7 @@ const StaticDateTimePicker = React.forwardRef(function StaticDateTimePicker >(inProps, 'MuiStaticDateTimePicker'); diff --git a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.types.ts b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.types.ts index f32150d6c35e..8ac7948fd2e4 100644 --- a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.types.ts +++ b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.types.ts @@ -20,7 +20,7 @@ export interface StaticDateTimePickerSlotsComponentsProps UseStaticPickerSlotsComponentsProps {} export interface StaticDateTimePickerProps - extends BaseDateTimePickerProps, + extends BaseDateTimePickerProps, MakeOptional { /** * Overridable components. diff --git a/packages/x-date-pickers/src/TimeClock/tests/describes.TimeClock.test.tsx b/packages/x-date-pickers/src/TimeClock/tests/describes.TimeClock.test.tsx index 9938e53d561a..12fb9ce5cf07 100644 --- a/packages/x-date-pickers/src/TimeClock/tests/describes.TimeClock.test.tsx +++ b/packages/x-date-pickers/src/TimeClock/tests/describes.TimeClock.test.tsx @@ -31,13 +31,10 @@ describe(' - Describes', () => { render, componentFamily: 'clock', values: [ - adapterToUse.date(new Date(2018, 0, 1, 15, 30)), - adapterToUse.date(new Date(2018, 0, 1, 18, 30)), + adapterToUse.date(new Date(2018, 0, 1, 12, 30)), + adapterToUse.date(new Date(2018, 0, 1, 13, 35)), ], emptyValue: null, - defaultProps: { - openTo: 'minutes', - }, clock, assertRenderedValue: (expectedValue: any) => { const clockPointer = document.querySelector(`.${clockPointerClasses.root}`); @@ -45,14 +42,32 @@ describe(' - Describes', () => { expect(clockPointer).to.equal(null); } else { const transform = clockPointer?.style?.transform; - expect(transform).to.equal(`rotateZ(${adapterToUse.getMinutes(expectedValue) * 6}deg)`); + const isMinutesView = screen + .getByRole('listbox') + .getAttribute('aria-label') + ?.includes('minutes'); + if (isMinutesView) { + expect(transform).to.equal(`rotateZ(${adapterToUse.getMinutes(expectedValue) * 6}deg)`); + } else { + const hours = adapterToUse.getHours(expectedValue); + expect(transform).to.equal(`rotateZ(${(hours > 12 ? hours % 12 : hours) * 30}deg)`); + } } }, setNewValue: (value) => { - const newValue = adapterToUse.addMinutes(value, 1); - const hourClockEvent = getClockTouchEvent(adapterToUse.getMinutes(newValue), 'minutes'); + const newValue = adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 5); + const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); + // change hours + const hourClockEvent = getClockTouchEvent( + adapterToUse.getHours(newValue), + hasMeridiem ? '12hours' : '24hours', + ); fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', hourClockEvent); fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', hourClockEvent); + // change minutes + const minutesClockEvent = getClockTouchEvent(adapterToUse.getMinutes(newValue), 'minutes'); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', minutesClockEvent); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', minutesClockEvent); return newValue; }, diff --git a/packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx b/packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx new file mode 100644 index 000000000000..de38812ef2b9 --- /dev/null +++ b/packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx @@ -0,0 +1,181 @@ +import * as React from 'react'; +import Divider from '@mui/material/Divider'; +import { resolveComponentProps } from '@mui/base/utils'; +import { DateCalendar, DateCalendarProps } from '../DateCalendar'; +import { DateOrTimeViewWithMeridiem } from '../internals/models'; +import { + MultiSectionDigitalClock, + MultiSectionDigitalClockProps, + multiSectionDigitalClockSectionClasses, +} from '../MultiSectionDigitalClock'; +import { DateTimeViewWrapper } from '../internals/components/DateTimeViewWrapper'; +import { isInternalTimeView } from '../internals/utils/time-utils'; +import { isDatePickerView } from '../internals/utils/date-utils'; +import type { DateTimePickerProps } from '../DateTimePicker/DateTimePicker.types'; + +export interface DateTimeViewRendererProps + extends Omit< + DateCalendarProps & MultiSectionDigitalClockProps, + | 'views' + | 'openTo' + | 'view' + | 'onViewChange' + | 'focusedView' + | 'components' + | 'componentsProps' + | 'slots' + | 'slotProps' + >, + Pick, 'components' | 'componentsProps' | 'slots' | 'slotProps'> { + view: DateOrTimeViewWithMeridiem; + onViewChange?: (view: DateOrTimeViewWithMeridiem) => void; + views: readonly DateOrTimeViewWithMeridiem[]; + focusedView: DateOrTimeViewWithMeridiem | null; + timeViewsCount: number; +} + +export const renderDesktopDateTimeView = ({ + view, + onViewChange, + views, + focusedView, + onFocusedViewChange, + value, + defaultValue, + onChange, + className, + classes, + disableFuture, + disablePast, + minDate, + minTime, + maxDate, + maxTime, + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + shouldDisableTime, + shouldDisableClock, + reduceAnimations, + minutesStep, + ampm, + onMonthChange, + monthsPerRow, + onYearChange, + yearsPerRow, + defaultCalendarMonth, + components, + componentsProps, + slots, + slotProps, + loading, + renderLoading, + disableHighlightToday, + readOnly, + disabled, + showDaysOutsideCurrentMonth, + dayOfWeekFormatter, + sx, + autoFocus, + fixedWeekNumber, + displayWeekNumber, + disableIgnoringDatePartForTimeValidation, + timeSteps, + skipDisabled, + timeViewsCount, +}: DateTimeViewRendererProps) => { + const isActionBarVisible = !!resolveComponentProps( + slotProps?.actionBar ?? componentsProps?.actionBar, + {} as any, + )?.actions?.length; + return ( + + + + {timeViewsCount > 0 && ( + + + + + )} + + {isActionBarVisible && } + + ); +}; diff --git a/packages/x-date-pickers/src/dateTimeViewRenderers/index.ts b/packages/x-date-pickers/src/dateTimeViewRenderers/index.ts new file mode 100644 index 000000000000..d909595c47ae --- /dev/null +++ b/packages/x-date-pickers/src/dateTimeViewRenderers/index.ts @@ -0,0 +1,2 @@ +export { renderDesktopDateTimeView } from './dateTimeViewRenderers'; +export type { DateTimeViewRendererProps } from './dateTimeViewRenderers'; diff --git a/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx b/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx index f5e73ed1d6ec..0a6a23a64507 100644 --- a/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx +++ b/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx @@ -2,9 +2,7 @@ import * as React from 'react'; import { DateCalendar, DateCalendarProps } from '../DateCalendar'; import { DateView } from '../models'; import { DateOrTimeViewWithMeridiem } from '../internals/models'; - -const isDatePickerView = (view: unknown): view is DateView => - view === 'year' || view === 'month' || view === 'day'; +import { isDatePickerView } from '../internals/utils/date-utils'; export interface DateViewRendererProps extends Omit< @@ -56,12 +54,12 @@ export const renderDateViewCalendar = ({ autoFocus, fixedWeekNumber, displayWeekNumber, -}: DateViewRendererProps) => ( +}: DateViewRendererProps) => ( ; + width?: number; } const useUtilityClasses = (ownerState: PickersToolbarButtonProps) => { @@ -40,7 +41,8 @@ const PickersToolbarButtonRoot = styled(Button, { export const PickersToolbarButton: React.FunctionComponent = React.forwardRef(function PickersToolbarButton(inProps, ref) { const props = useThemeProps({ props: inProps, name: 'MuiPickersToolbarButton' }); - const { align, className, selected, typographyClassName, value, variant, ...other } = props; + const { align, className, selected, typographyClassName, value, variant, width, ...other } = + props; const classes = useUtilityClasses(props); @@ -50,6 +52,7 @@ export const PickersToolbarButton: React.FunctionComponent { - const numberUITimeViews = views.reduce((acc, viewForReduce) => { - if (viewRenderers[viewForReduce] != null && isTimeView(viewForReduce)) { - return acc + 1; - } - return acc; - }, 0); - - return numberUITimeViews > 1; - }, [viewRenderers, views]); + const timeViewsCount = React.useMemo( + () => + views.reduce((acc, viewForReduce) => { + if (viewRenderers[viewForReduce] != null && isTimeView(viewForReduce)) { + return acc + 1; + } + return acc; + }, 0), + [viewRenderers, views], + ); const currentViewMode = viewModeLookup[view]; const shouldRestoreFocus = useEventCallback(() => currentViewMode === 'UI'); @@ -270,7 +270,8 @@ export const usePickerViews = < onViewChange: setView, focusedView, onFocusedViewChange: setFocusedView, - showViewSwitcher: hasMultipleUITimeView, + showViewSwitcher: timeViewsCount > 1, + timeViewsCount, }); }, }; diff --git a/packages/x-date-pickers/src/internals/hooks/useViews.tsx b/packages/x-date-pickers/src/internals/hooks/useViews.tsx index 649f8ee37a22..1555a56a7e96 100644 --- a/packages/x-date-pickers/src/internals/hooks/useViews.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useViews.tsx @@ -147,17 +147,6 @@ export function useViews({ const previousView: TView | null = views[viewIndex - 1] ?? null; const nextView: TView | null = views[viewIndex + 1] ?? null; - const handleChangeView = useEventCallback((newView: TView) => { - if (newView === view) { - return; - } - setView(newView); - - if (onViewChange) { - onViewChange(newView); - } - }); - const handleFocusedViewChange = useEventCallback((viewToFocus: TView, hasFocus: boolean) => { if (hasFocus) { // Focus event @@ -172,6 +161,17 @@ export function useViews({ onFocusedViewChange?.(viewToFocus, hasFocus); }); + const handleChangeView = useEventCallback((newView: TView) => { + if (newView === view) { + return; + } + setView(newView); + handleFocusedViewChange(newView, true); + + if (onViewChange) { + onViewChange(newView); + } + }); const goToNextView = useEventCallback(() => { if (nextView) { handleChangeView(nextView); diff --git a/packages/x-date-pickers/src/internals/utils/date-utils.ts b/packages/x-date-pickers/src/internals/utils/date-utils.ts index 21e1f29acdc5..a9edff13431a 100644 --- a/packages/x-date-pickers/src/internals/utils/date-utils.ts +++ b/packages/x-date-pickers/src/internals/utils/date-utils.ts @@ -1,4 +1,5 @@ -import { FieldValueType, MuiPickersAdapter } from '../../models'; +import { DateView, FieldValueType, MuiPickersAdapter } from '../../models'; +import { DateOrTimeViewWithMeridiem } from '../models'; interface FindClosestDateParams { date: TDate; @@ -137,3 +138,7 @@ export const mergeDateAndTime = ( export const getTodayDate = (utils: MuiPickersAdapter, valueType: FieldValueType) => valueType === 'date' ? utils.startOfDay(utils.date()!) : utils.date()!; + +const dateViews = ['year', 'month', 'day']; +export const isDatePickerView = (view: DateOrTimeViewWithMeridiem): view is DateView => + dateViews.includes(view); diff --git a/packages/x-date-pickers/src/internals/utils/time-utils.ts b/packages/x-date-pickers/src/internals/utils/time-utils.ts index 7710220e78ab..877632c33769 100644 --- a/packages/x-date-pickers/src/internals/utils/time-utils.ts +++ b/packages/x-date-pickers/src/internals/utils/time-utils.ts @@ -1,9 +1,13 @@ import { MuiPickersAdapter } from '../../models'; -import { DateOrTimeViewWithMeridiem } from '../models'; +import { DateOrTimeViewWithMeridiem, TimeViewWithMeridiem } from '../models'; const timeViews = ['hours', 'minutes', 'seconds']; export const isTimeView = (view: DateOrTimeViewWithMeridiem) => timeViews.includes(view); +export const isInternalTimeView = ( + view: DateOrTimeViewWithMeridiem, +): view is TimeViewWithMeridiem => timeViews.includes(view) || view === 'meridiem'; + export type Meridiem = 'am' | 'pm'; export const getMeridiem = ( diff --git a/packages/x-date-pickers/src/tests/describeValidation/testDayViewValidation.tsx b/packages/x-date-pickers/src/tests/describeValidation/testDayViewValidation.tsx index 555445d09467..1268da250106 100644 --- a/packages/x-date-pickers/src/tests/describeValidation/testDayViewValidation.tsx +++ b/packages/x-date-pickers/src/tests/describeValidation/testDayViewValidation.tsx @@ -17,11 +17,7 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest open: true, view: 'day', reduceAnimations: true, - ...(componentFamily.includes('legacy-') - ? { - componentsProps: { toolbar: { hidden: true } }, - } - : { slotProps: { toolbar: { hidden: true } } }), + slotProps: { toolbar: { hidden: true } }, }; it('should apply shouldDisableDate', function test() { @@ -35,10 +31,10 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest />, ); - expect(screen.getByText('9')).not.to.have.attribute('disabled'); - expect(screen.getByText('10')).not.to.have.attribute('disabled'); - expect(screen.getByText('11')).to.have.attribute('disabled'); - expect(screen.getByText('12')).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '9' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '10' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '11' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '12' })).to.have.attribute('disabled'); }); it('should apply shouldDisableYear', function test() { @@ -50,16 +46,16 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest />, ); - expect(screen.getByText('1')).to.have.attribute('disabled'); - expect(screen.getByText('15')).to.have.attribute('disabled'); - expect(screen.getByText('30')).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '15' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '30' })).to.have.attribute('disabled'); setProps({ value: adapterToUse.date(new Date(2019, 0, 1)) }); clock.runToLast(); - expect(screen.getByText('1')).not.to.have.attribute('disabled'); - expect(screen.getByText('15')).not.to.have.attribute('disabled'); - expect(screen.getByText('30')).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '15' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '30' })).not.to.have.attribute('disabled'); }); it('should apply shouldDisableMonth', function test() { @@ -71,16 +67,16 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest />, ); - expect(screen.getByText('1')).to.have.attribute('disabled'); - expect(screen.getByText('15')).to.have.attribute('disabled'); - expect(screen.getByText('30')).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '15' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '30' })).to.have.attribute('disabled'); setProps({ value: adapterToUse.date(new Date(2018, 1, 1)) }); clock.runToLast(); - expect(screen.getByText('1')).not.to.have.attribute('disabled'); - expect(screen.getByText('15')).not.to.have.attribute('disabled'); - expect(screen.getByText('28')).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '15' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '28' })).not.to.have.attribute('disabled'); }); it('should apply disablePast', function test() { @@ -94,25 +90,25 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest const tomorrow = adapterToUse.addDays(now, 1); const yesterday = adapterToUse.addDays(now, -1); - expect(screen.getByText(adapterToUse.format(now, 'dayOfMonth'))).not.to.have.attribute( - 'disabled', - ); + expect( + screen.getByRole('gridcell', { name: adapterToUse.format(now, 'dayOfMonth') }), + ).not.to.have.attribute('disabled'); if (!adapterToUse.isSameMonth(now, tomorrow)) { setProps({ value: tomorrow }); clock.runToLast(); } - expect(screen.getByText(adapterToUse.format(tomorrow, 'dayOfMonth'))).not.to.have.attribute( - 'disabled', - ); + expect( + screen.getByRole('gridcell', { name: adapterToUse.format(tomorrow, 'dayOfMonth') }), + ).not.to.have.attribute('disabled'); if (!adapterToUse.isSameMonth(yesterday, tomorrow)) { setProps({ value: yesterday }); clock.runToLast(); } - expect(screen.getByText(adapterToUse.format(yesterday, 'dayOfMonth'))).to.have.attribute( - 'disabled', - ); + expect( + screen.getByRole('gridcell', { name: adapterToUse.format(yesterday, 'dayOfMonth') }), + ).to.have.attribute('disabled'); }); it('should apply disableFuture', function test() { @@ -126,25 +122,25 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest const tomorrow = adapterToUse.addDays(now, 1); const yesterday = adapterToUse.addDays(now, -1); - expect(screen.getByText(adapterToUse.format(now, 'dayOfMonth'))).not.to.have.attribute( - 'disabled', - ); + expect( + screen.getByRole('gridcell', { name: adapterToUse.format(now, 'dayOfMonth') }), + ).not.to.have.attribute('disabled'); if (!adapterToUse.isSameMonth(now, tomorrow)) { setProps({ value: tomorrow }); clock.runToLast(); } - expect(screen.getByText(adapterToUse.format(tomorrow, 'dayOfMonth'))).to.have.attribute( - 'disabled', - ); + expect( + screen.getByRole('gridcell', { name: adapterToUse.format(tomorrow, 'dayOfMonth') }), + ).to.have.attribute('disabled'); if (!adapterToUse.isSameMonth(yesterday, tomorrow)) { setProps({ value: yesterday }); clock.runToLast(); } - expect(screen.getByText(adapterToUse.format(yesterday, 'dayOfMonth'))).not.to.have.attribute( - 'disabled', - ); + expect( + screen.getByRole('gridcell', { name: adapterToUse.format(yesterday, 'dayOfMonth') }), + ).not.to.have.attribute('disabled'); }); it('should apply minDate', function test() { @@ -155,11 +151,11 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest minDate={adapterToUse.date(new Date(2019, 5, 4))} />, ); - expect(screen.getByText('1')).to.have.attribute('disabled'); - expect(screen.getByText('3')).to.have.attribute('disabled'); - expect(screen.getByText('4')).not.to.have.attribute('disabled'); - expect(screen.getByText('5')).not.to.have.attribute('disabled'); - expect(screen.getByText('30')).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '3' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '4' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '5' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '30' })).not.to.have.attribute('disabled'); expect(screen.getByLabelText('Previous month')).to.have.attribute('disabled'); expect(screen.getByLabelText('Next month')).not.to.have.attribute('disabled'); }); @@ -172,11 +168,11 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest maxDate={adapterToUse.date(new Date(2019, 5, 4))} />, ); - expect(screen.getByText('1')).not.to.have.attribute('disabled'); - expect(screen.getByText('3')).not.to.have.attribute('disabled'); - expect(screen.getByText('4')).not.to.have.attribute('disabled'); - expect(screen.getByText('5')).to.have.attribute('disabled'); - expect(screen.getByText('30')).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '3' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '4' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '5' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '30' })).to.have.attribute('disabled'); expect(screen.getByLabelText('Previous month')).not.to.have.attribute('disabled'); expect(screen.getByLabelText('Next month')).to.have.attribute('disabled'); }); @@ -194,11 +190,11 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest maxDateTime={adapterToUse.date(new Date(2019, 5, 4, 12, 0, 0))} />, ); - expect(screen.getByText('1')).not.to.have.attribute('disabled'); - expect(screen.getByText('3')).not.to.have.attribute('disabled'); - expect(screen.getByText('4')).not.to.have.attribute('disabled'); - expect(screen.getByText('5')).to.have.attribute('disabled'); - expect(screen.getByText('30')).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '3' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '4' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '5' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '30' })).to.have.attribute('disabled'); }); it('should apply minDateTime', function test() { @@ -214,11 +210,11 @@ export const testDayViewValidation: DescribeValidationTestSuite = (ElementToTest minDateTime={adapterToUse.date(new Date(2019, 5, 4, 12, 0, 0))} />, ); - expect(screen.getByText('1')).to.have.attribute('disabled'); - expect(screen.getByText('3')).to.have.attribute('disabled'); - expect(screen.getByText('4')).not.to.have.attribute('disabled'); - expect(screen.getByText('5')).not.to.have.attribute('disabled'); - expect(screen.getByText('30')).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '1' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '3' })).to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '4' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '5' })).not.to.have.attribute('disabled'); + expect(screen.getByRole('gridcell', { name: '30' })).not.to.have.attribute('disabled'); }); }); }; diff --git a/packages/x-date-pickers/src/tests/describeValidation/testMinutesViewValidation.tsx b/packages/x-date-pickers/src/tests/describeValidation/testMinutesViewValidation.tsx index 7450de4d9614..5d81ffdcac67 100644 --- a/packages/x-date-pickers/src/tests/describeValidation/testMinutesViewValidation.tsx +++ b/packages/x-date-pickers/src/tests/describeValidation/testMinutesViewValidation.tsx @@ -29,11 +29,7 @@ export const testMinutesViewValidation: DescribeValidationTestSuite = ( view: 'minutes', openTo: 'minutes', reduceAnimations: true, - ...(componentFamily.includes('legacy-') - ? { - componentsProps: { toolbar: { hidden: true } }, - } - : { slotProps: { toolbar: { hidden: true } } }), + slotProps: { toolbar: { hidden: true } }, }; it('should apply shouldDisableTime', function test() { diff --git a/packages/x-date-pickers/src/tests/describeValidation/testMonthViewValidation.tsx b/packages/x-date-pickers/src/tests/describeValidation/testMonthViewValidation.tsx index 49d55aec623b..1dee3a03455a 100644 --- a/packages/x-date-pickers/src/tests/describeValidation/testMonthViewValidation.tsx +++ b/packages/x-date-pickers/src/tests/describeValidation/testMonthViewValidation.tsx @@ -22,11 +22,7 @@ export const testMonthViewValidation: DescribeValidationTestSuite = (ElementToTe ...(componentFamily !== 'calendar' && { open: true, reduceAnimations: true, - ...(componentFamily.includes('legacy-') - ? { - componentsProps: { toolbar: { hidden: true } }, - } - : { slotProps: { toolbar: { hidden: true } } }), + slotProps: { toolbar: { hidden: true } }, }), }; diff --git a/packages/x-date-pickers/src/tests/describeValidation/testYearViewValidation.tsx b/packages/x-date-pickers/src/tests/describeValidation/testYearViewValidation.tsx index 45bedd32abb4..43c5b19dd86d 100644 --- a/packages/x-date-pickers/src/tests/describeValidation/testYearViewValidation.tsx +++ b/packages/x-date-pickers/src/tests/describeValidation/testYearViewValidation.tsx @@ -22,11 +22,7 @@ export const testYearViewValidation: DescribeValidationTestSuite = (ElementToTes ...(componentFamily !== 'calendar' && { open: true, reduceAnimations: true, - ...(componentFamily.includes('legacy-') - ? { - componentsProps: { toolbar: { hidden: true } }, - } - : { slotProps: { toolbar: { hidden: true } } }), + slotProps: { toolbar: { hidden: true } }, }), }; diff --git a/packages/x-date-pickers/src/tests/describeValue/describeValue.tsx b/packages/x-date-pickers/src/tests/describeValue/describeValue.tsx index ddc2690e46d7..16ae7bbd061a 100644 --- a/packages/x-date-pickers/src/tests/describeValue/describeValue.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/describeValue.tsx @@ -24,20 +24,22 @@ function innerDescribeValue( function WrappedElementToTest( props: BasePickerInputProps & - UsePickerValueNonStaticProps, + UsePickerValueNonStaticProps & { hook?: any }, ) { - return ; + const { hook, ...other } = props; + const hookResult = hook?.(props); + return ; } let renderWithProps: BuildFieldInteractionsResponse['renderWithProps']; if (componentFamily === 'field' || componentFamily === 'picker') { const interactions = buildFieldInteractions({ clock, render, Component: ElementToTest }); - renderWithProps = (props: any) => - interactions.renderWithProps({ ...defaultProps, ...props }, componentFamily); + renderWithProps = (props: any, hook?: any) => + interactions.renderWithProps({ ...defaultProps, ...props }, hook, componentFamily); } else { - renderWithProps = (props: any) => { - const response = render(); + renderWithProps = (props: any, hook?: any) => { + const response = render(); return { ...response, diff --git a/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx b/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx index a814da564420..1c9a52114b38 100644 --- a/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/testControlledUnControlled.tsx @@ -53,7 +53,7 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( assertRenderedValue(newValue); // TODO: Clean this exception or change the clock behavior - expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily)); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, params)); if (Array.isArray(newValue)) { newValue.forEach((value, index) => { expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); @@ -66,10 +66,25 @@ export const testControlledUnControlled: DescribeValueTestSuite = ( it('should call onChange when updating a value defined with `props.value`', () => { const onChange = spy(); - const { selectSection } = renderWithProps({ defaultValue: values[0], onChange }); + const useControlledElement = (props) => { + const [value, setValue] = React.useState(props?.value || null); + const handleChange = React.useCallback( + (newValue) => { + setValue(newValue); + props?.onChange(newValue); + }, + [props], + ); + return { value, onChange: handleChange }; + }; + + const { selectSection } = renderWithProps( + { value: values[0], onChange }, + useControlledElement, + ); const newValue = setNewValue(values[0], { selectSection }); - expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily)); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, params)); if (Array.isArray(newValue)) { newValue.forEach((value, index) => { expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); diff --git a/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx b/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx index ab4bb8d57de4..33fa18ce55c2 100644 --- a/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/testPickerActionBar.tsx @@ -2,15 +2,22 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { screen, userEvent } from '@mui/monorepo/test/utils'; -import { adapterToUse } from 'test/utils/pickers-utils'; +import { adapterToUse, getExpectedOnChangeCount } from 'test/utils/pickers-utils'; import { DescribeValueTestSuite } from './describeValue.types'; export const testPickerActionBar: DescribeValueTestSuite = ( ElementToTest, options, ) => { - const { componentFamily, render, renderWithProps, values, emptyValue, setNewValue, type } = - options; + const { + componentFamily, + render, + renderWithProps, + values, + emptyValue, + setNewValue, + ...pickerParams + } = options; if (componentFamily !== 'picker') { return; @@ -37,7 +44,7 @@ export const testPickerActionBar: DescribeValueTestSuite = ( // Clear the date userEvent.mousePress(screen.getByText(/clear/i)); expect(onChange.callCount).to.equal(1); - if (type === 'date-range') { + if (pickerParams.type === 'date-range') { onChange.lastCall.args[0].forEach((value, index) => { expect(value).to.deep.equal(emptyValue[index]); }); @@ -45,7 +52,7 @@ export const testPickerActionBar: DescribeValueTestSuite = ( expect(onChange.lastCall.args[0]).to.deep.equal(emptyValue); } expect(onAccept.callCount).to.equal(1); - if (type === 'date-range') { + if (pickerParams.type === 'date-range') { onAccept.lastCall.args[0].forEach((value, index) => { expect(value).to.deep.equal(emptyValue[index]); }); @@ -100,8 +107,10 @@ export const testPickerActionBar: DescribeValueTestSuite = ( // Cancel the modifications userEvent.mousePress(screen.getByText(/cancel/i)); - expect(onChange.callCount).to.equal(2); - if (type === 'date-range') { + expect(onChange.callCount).to.equal( + getExpectedOnChangeCount(componentFamily, pickerParams) + 1, + ); + if (pickerParams.type === 'date-range') { values[0].forEach((value, index) => { expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); }); @@ -158,7 +167,9 @@ export const testPickerActionBar: DescribeValueTestSuite = ( // Accept the modifications userEvent.mousePress(screen.getByText(/ok/i)); - expect(onChange.callCount).to.equal(1); // The accepted value as already been committed, don't call onChange again + expect(onChange.callCount).to.equal( + getExpectedOnChangeCount(componentFamily, pickerParams), + ); // The accepted value as already been committed, don't call onChange again expect(onAccept.callCount).to.equal(1); expect(onClose.callCount).to.equal(1); }); @@ -232,10 +243,12 @@ export const testPickerActionBar: DescribeValueTestSuite = ( userEvent.mousePress(screen.getByText(/today/i)); const startOfToday = - type === 'date' ? adapterToUse.startOfDay(adapterToUse.date()) : adapterToUse.date(); + pickerParams.type === 'date' + ? adapterToUse.startOfDay(adapterToUse.date()) + : adapterToUse.date(); expect(onChange.callCount).to.equal(1); - if (type === 'date-range') { + if (pickerParams.type === 'date-range') { onChange.lastCall.args[0].forEach((value) => { expect(value).toEqualDateTime(startOfToday); }); @@ -243,7 +256,7 @@ export const testPickerActionBar: DescribeValueTestSuite = ( expect(onChange.lastCall.args[0]).toEqualDateTime(startOfToday); } expect(onAccept.callCount).to.equal(1); - if (type === 'date-range') { + if (pickerParams.type === 'date-range') { onAccept.lastCall.args[0].forEach((value) => { expect(value).toEqualDateTime(startOfToday); }); diff --git a/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx b/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx index 24642d7f5570..96b622282171 100644 --- a/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx +++ b/packages/x-date-pickers/src/tests/describeValue/testPickerOpenCloseLifeCycle.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; import { screen, userEvent } from '@mui/monorepo/test/utils'; -import { getTextbox, openPicker } from 'test/utils/pickers-utils'; +import { getExpectedOnChangeCount, getTextbox, openPicker } from 'test/utils/pickers-utils'; import { DescribeValueTestSuite } from './describeValue.types'; export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite = ( @@ -65,7 +65,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Change the value let newValue = setNewValue(values[0], { isOpened: true, selectSection }); - expect(onChange.callCount).to.equal(1); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, pickerParams)); if (pickerParams.type === 'date-range') { newValue = setNewValue(newValue, { isOpened: true, setEndDate: true, selectSection }); newValue.forEach((value, index) => { @@ -116,7 +116,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Change the value let newValue = setNewValue(values[0], { isOpened: true, selectSection }); - expect(onChange.callCount).to.equal(1); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, pickerParams)); if (pickerParams.type === 'date-range') { newValue = setNewValue(newValue, { isOpened: true, setEndDate: true, selectSection }); newValue.forEach((value, index) => { @@ -175,7 +175,8 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Change the value let newValue = setNewValue(values[0], { isOpened: true, selectSection }); - expect(onChange.callCount).to.equal(1); + const initialChangeCount = getExpectedOnChangeCount(componentFamily, pickerParams); + expect(onChange.callCount).to.equal(initialChangeCount); if (pickerParams.type === 'date-range') { newValue = setNewValue(newValue, { isOpened: true, setEndDate: true, selectSection }); newValue.forEach((value, index) => { @@ -196,7 +197,12 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite expect(onChange.lastCall.args[0][index]).toEqualDateTime(value); }); } else { - expect(onChange.callCount).to.equal(2); + expect(onChange.callCount).to.equal( + initialChangeCount + + getExpectedOnChangeCount(componentFamily, pickerParams) - + // meridiem does not change this time in case of multi section digital clock + (pickerParams.type === 'time' || pickerParams.type === 'date-time' ? 1 : 0), + ); expect(onChange.lastCall.args[0]).toEqualDateTime(newValueBis as any); } expect(onAccept.callCount).to.equal(0); @@ -222,7 +228,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Dismiss the picker userEvent.keyPress(document.activeElement!, { key: 'Escape' }); - expect(onChange.callCount).to.equal(1); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, pickerParams)); expect(onAccept.callCount).to.equal(1); if (pickerParams.type === 'date-range') { newValue.forEach((value, index) => { @@ -286,7 +292,7 @@ export const testPickerOpenCloseLifeCycle: DescribeValueTestSuite // Dismiss the picker userEvent.mousePress(document.body); - expect(onChange.callCount).to.equal(1); + expect(onChange.callCount).to.equal(getExpectedOnChangeCount(componentFamily, pickerParams)); expect(onAccept.callCount).to.equal(1); expect(onAccept.lastCall.args[0]).toEqualDateTime(newValue as any); expect(onClose.callCount).to.equal(1); diff --git a/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx b/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx index 0e7162d692db..539511d8c35e 100644 --- a/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx +++ b/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx @@ -50,13 +50,13 @@ export const renderTimeViewClock = ({ autoFocus, showViewSwitcher, disableIgnoringDatePartForTimeValidation, -}: TimeViewRendererProps>) => ( +}: TimeViewRendererProps>) => ( - view={view as TimeView} + view={view} onViewChange={onViewChange} - focusedView={focusedView as TimeView} + focusedView={focusedView && isTimeView(focusedView) ? focusedView : null} onFocusedViewChange={onFocusedViewChange} - views={views.filter(isTimeView) as TimeView[]} + views={views.filter(isTimeView)} value={value} defaultValue={defaultValue} onChange={onChange} diff --git a/test/utils/pickers-utils.tsx b/test/utils/pickers-utils.tsx index 5875ffd84b88..3ccf62ffae11 100644 --- a/test/utils/pickers-utils.tsx +++ b/test/utils/pickers-utils.tsx @@ -366,6 +366,7 @@ export type FieldSectionSelector = ( export interface BuildFieldInteractionsResponse

{ renderWithProps: ( props: P, + hook?: (props: P) => Record, componentFamily?: 'picker' | 'field', ) => ReturnType['render']> & { input: HTMLInputElement; @@ -419,13 +420,19 @@ export const buildFieldInteractions =

({ }); }; - const renderWithProps = (props: P, componentFamily: 'picker' | 'field' = 'field') => { + const renderWithProps: BuildFieldInteractionsResponse

['renderWithProps'] = ( + props, + hook, + componentFamily = 'field', + ) => { let fieldRef: React.RefObject> = { current: null }; function WrappedComponent() { fieldRef = React.useRef>(null); + const hookResult = hook?.(props); const allProps = { ...props, + ...hookResult, } as any; if (componentFamily === 'field') { @@ -615,13 +622,40 @@ export class MockedDataTransfer implements DataTransfer { } } -export const getExpectedOnChangeCount = (componentFamily: PickerComponentFamily) => { +const getChangeCountForComponentFamily = (componentFamily: PickerComponentFamily) => { switch (componentFamily) { case 'clock': - return 2; case 'multi-section-digital-clock': return 3; default: return 1; } }; + +export const getExpectedOnChangeCount = ( + componentFamily: PickerComponentFamily, + params: OpenPickerParams, +) => { + if (componentFamily === 'digital-clock') { + return getChangeCountForComponentFamily(componentFamily); + } + if (params.type === 'date-time') { + return ( + getChangeCountForComponentFamily(componentFamily) + + getChangeCountForComponentFamily( + params.variant === 'desktop' ? 'multi-section-digital-clock' : 'clock', + ) + ); + } + if (componentFamily === 'picker' && params.type === 'time') { + return getChangeCountForComponentFamily( + params.variant === 'desktop' ? 'multi-section-digital-clock' : 'clock', + ); + } + if (componentFamily === 'clock') { + // the `TimeClock` fires change for both touch move and touch end + // but does not have meridiem control + return (getChangeCountForComponentFamily(componentFamily) - 1) * 2; + } + return getChangeCountForComponentFamily(componentFamily); +};