From b53ee135f651ee815b64365182c5ea537bce18c3 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Fri, 30 Jun 2023 11:12:33 +0200 Subject: [PATCH] [pickers] Add `referenceDate` prop on `TimeClock`, `DigitalClock` and `MultiSectionDigitalClock` (#9356) --- .../x/api/date-pickers/digital-clock.json | 4 + .../x/api/date-pickers/month-calendar.json | 2 +- .../multi-section-digital-clock.json | 4 + docs/pages/x/api/date-pickers/time-clock.json | 4 + .../x/api/date-pickers/year-calendar.json | 2 +- .../api-docs/date-pickers/digital-clock.json | 1 + .../multi-section-digital-clock.json | 1 + .../api-docs/date-pickers/time-clock.json | 1 + .../src/AdapterDayjs/AdapterDayjs.ts | 1 - .../DateCalendar/tests/DateCalendar.test.tsx | 34 ++++---- .../src/DigitalClock/DigitalClock.tsx | 30 ++++--- .../DigitalClock/tests/DigitalClock.test.tsx | 68 +++++++++++++++ .../tests/describes.DigitalClock.test.tsx | 10 +-- .../src/MonthCalendar/MonthCalendar.tsx | 2 +- .../src/MonthCalendar/MonthCalendar.types.ts | 2 +- .../MultiSectionDigitalClock.tsx | 55 ++++++------ .../tests/MultiSectionDigitalClock.test.tsx | 86 +++++++++++++++++++ ...escribes.MultiSectionDigitalClock.test.tsx | 19 +--- .../src/TimeClock/TimeClock.tsx | 59 ++++++++----- .../TimeClock/{ => tests}/TimeClock.test.tsx | 62 +++++++++++++ .../tests/describes.TimeClock.test.tsx | 25 ++---- .../src/YearCalendar/YearCalendar.tsx | 2 +- .../src/YearCalendar/YearCalendar.types.ts | 2 +- .../internals/hooks/useClockReferenceDate.ts | 35 ++++++++ .../hooks/usePicker/usePickerValue.types.ts | 2 + .../src/internals/models/props/clock.ts | 7 +- .../utils/getDefaultReferenceDate.ts | 11 ++- test/utils/pickers-utils.tsx | 7 +- test/utils/pickers/viewHandlers.ts | 58 +++++++++++++ 29 files changed, 468 insertions(+), 128 deletions(-) create mode 100644 packages/x-date-pickers/src/DigitalClock/tests/DigitalClock.test.tsx create mode 100644 packages/x-date-pickers/src/MultiSectionDigitalClock/tests/MultiSectionDigitalClock.test.tsx rename packages/x-date-pickers/src/TimeClock/{ => tests}/TimeClock.test.tsx (89%) create mode 100644 packages/x-date-pickers/src/internals/hooks/useClockReferenceDate.ts create mode 100644 test/utils/pickers/viewHandlers.ts diff --git a/docs/pages/x/api/date-pickers/digital-clock.json b/docs/pages/x/api/date-pickers/digital-clock.json index e7840930fb61..03e77ddbb032 100644 --- a/docs/pages/x/api/date-pickers/digital-clock.json +++ b/docs/pages/x/api/date-pickers/digital-clock.json @@ -29,6 +29,10 @@ "onViewChange": { "type": { "name": "func" } }, "openTo": { "type": { "name": "enum", "description": "'hours'" } }, "readOnly": { "type": { "name": "bool" } }, + "referenceDate": { + "type": { "name": "any" }, + "default": "The closest valid time using the validation props, except callbacks such as `shouldDisableTime`." + }, "shouldDisableClock": { "type": { "name": "func" }, "deprecated": true, diff --git a/docs/pages/x/api/date-pickers/month-calendar.json b/docs/pages/x/api/date-pickers/month-calendar.json index 2450e99c4d57..7c5a811ff9bf 100644 --- a/docs/pages/x/api/date-pickers/month-calendar.json +++ b/docs/pages/x/api/date-pickers/month-calendar.json @@ -17,7 +17,7 @@ "readOnly": { "type": { "name": "bool" } }, "referenceDate": { "type": { "name": "any" }, - "default": "The closest valid month using the validation props, except callbacks such as `shouldDisableDate`." + "default": "The closest valid month using the validation props, except callbacks such as `shouldDisableMonth`." }, "shouldDisableMonth": { "type": { "name": "func" } }, "sx": { diff --git a/docs/pages/x/api/date-pickers/multi-section-digital-clock.json b/docs/pages/x/api/date-pickers/multi-section-digital-clock.json index 9c89cc98e924..52ba51b3fb8d 100644 --- a/docs/pages/x/api/date-pickers/multi-section-digital-clock.json +++ b/docs/pages/x/api/date-pickers/multi-section-digital-clock.json @@ -39,6 +39,10 @@ } }, "readOnly": { "type": { "name": "bool" } }, + "referenceDate": { + "type": { "name": "any" }, + "default": "The closest valid time using the validation props, except callbacks such as `shouldDisableTime`." + }, "shouldDisableClock": { "type": { "name": "func" }, "deprecated": true, diff --git a/docs/pages/x/api/date-pickers/time-clock.json b/docs/pages/x/api/date-pickers/time-clock.json index 1c2f09a3a8ed..60b11342bd72 100644 --- a/docs/pages/x/api/date-pickers/time-clock.json +++ b/docs/pages/x/api/date-pickers/time-clock.json @@ -40,6 +40,10 @@ } }, "readOnly": { "type": { "name": "bool" } }, + "referenceDate": { + "type": { "name": "any" }, + "default": "The closest valid time using the validation props, except callbacks such as `shouldDisableTime`." + }, "shouldDisableClock": { "type": { "name": "func" }, "deprecated": true, diff --git a/docs/pages/x/api/date-pickers/year-calendar.json b/docs/pages/x/api/date-pickers/year-calendar.json index 37b1fdb3026e..6ac8535dcb8d 100644 --- a/docs/pages/x/api/date-pickers/year-calendar.json +++ b/docs/pages/x/api/date-pickers/year-calendar.json @@ -13,7 +13,7 @@ "readOnly": { "type": { "name": "bool" } }, "referenceDate": { "type": { "name": "any" }, - "default": "The closest valid year using the validation props, except callbacks such as `shouldDisableDate`." + "default": "The closest valid year using the validation props, except callbacks such as `shouldDisableYear`." }, "shouldDisableYear": { "type": { "name": "func" } }, "sx": { diff --git a/docs/translations/api-docs/date-pickers/digital-clock.json b/docs/translations/api-docs/date-pickers/digital-clock.json index ea2298693ff7..97e559a2dd14 100644 --- a/docs/translations/api-docs/date-pickers/digital-clock.json +++ b/docs/translations/api-docs/date-pickers/digital-clock.json @@ -20,6 +20,7 @@ "onViewChange": "Callback fired on view change.

Signature:
function(view: TView) => void
view: The new view.", "openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from views list.", "readOnly": "If true, the picker views and text field are read-only.", + "referenceDate": "The date used to generate the new value when both value and defaultValue are empty.", "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "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.", "skipDisabled": "If true, disabled digital clock items will not be rendered.", diff --git a/docs/translations/api-docs/date-pickers/multi-section-digital-clock.json b/docs/translations/api-docs/date-pickers/multi-section-digital-clock.json index fde75dbdadce..d3a0d1beaf35 100644 --- a/docs/translations/api-docs/date-pickers/multi-section-digital-clock.json +++ b/docs/translations/api-docs/date-pickers/multi-section-digital-clock.json @@ -20,6 +20,7 @@ "onViewChange": "Callback fired on view change.

Signature:
function(view: TView) => void
view: The new view.", "openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from views list.", "readOnly": "If true, the picker views and text field are read-only.", + "referenceDate": "The date used to generate the new value when both value and defaultValue are empty.", "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "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.", "skipDisabled": "If true, disabled digital clock items will not be rendered.", diff --git a/docs/translations/api-docs/date-pickers/time-clock.json b/docs/translations/api-docs/date-pickers/time-clock.json index bcce1ef33b7f..b39df37a7b4e 100644 --- a/docs/translations/api-docs/date-pickers/time-clock.json +++ b/docs/translations/api-docs/date-pickers/time-clock.json @@ -21,6 +21,7 @@ "onViewChange": "Callback fired on view change.

Signature:
function(view: TView) => void
view: The new view.", "openTo": "The default visible view. Used when the component view is not controlled. Must be a valid option from views list.", "readOnly": "If true, the picker views and text field are read-only.", + "referenceDate": "The date used to generate the new value when both value and defaultValue are empty.", "shouldDisableClock": "Disable specific clock time.

Signature:
function(clockValue: number, view: TimeView) => boolean
clockValue: The value to check.
view: The clock type of the timeValue.
returns (boolean): If true the time will be disabled.", "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.", "slotProps": "The props used for each component slot.", diff --git a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts index 07d42f216559..0f6f73c6ba70 100644 --- a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts +++ b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts @@ -271,7 +271,6 @@ export class AdapterDayjs implements MuiPickersAdapter { } let parsedValue: Dayjs; - if (timezone === 'UTC') { parsedValue = this.createUTCDate(value); } else if (timezone === 'system' || (timezone === 'default' && !this.hasTimezonePlugin())) { diff --git a/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx b/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx index c718e6203e94..acd462cec353 100644 --- a/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx +++ b/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx @@ -174,7 +174,7 @@ describe('', () => { userEvent.mousePress(screen.getByRole('gridcell', { name: '2' })); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 2)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 2)); }); it('should use `referenceDate` when no value defined', () => { @@ -190,7 +190,7 @@ describe('', () => { userEvent.mousePress(screen.getByRole('gridcell', { name: '2' })); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2018, 0, 2, 12, 30)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 2, 12, 30)); }); it('should not use `referenceDate` when a value is defined', () => { @@ -207,7 +207,7 @@ describe('', () => { userEvent.mousePress(screen.getByRole('gridcell', { name: '2' })); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 0, 2, 12, 20)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 2, 12, 20)); }); it('should not use `referenceDate` when a defaultValue is defined', () => { @@ -224,7 +224,7 @@ describe('', () => { userEvent.mousePress(screen.getByRole('gridcell', { name: '2' })); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 0, 2, 12, 20)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 2, 12, 20)); }); it('should keep the time of the currently provided date', () => { @@ -241,7 +241,7 @@ describe('', () => { userEvent.mousePress(screen.getByRole('gridcell', { name: '2' })); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime( + expect(onChange.lastCall.firstArg).toEqualDateTime( adapterToUse.date(new Date(2018, 0, 2, 11, 11, 11)), ); }); @@ -304,7 +304,7 @@ describe('', () => { fireEvent.click(april); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 6)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 6)); }); it('should respect minDate when selecting closest enabled date', () => { @@ -324,7 +324,7 @@ describe('', () => { fireEvent.click(april); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 7)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 7)); }); it('should respect maxDate when selecting closest enabled date', () => { @@ -344,7 +344,7 @@ describe('', () => { fireEvent.click(april); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 22)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 22)); }); it('should go to next view without changing the date when no date of the new month is enabled', () => { @@ -384,7 +384,7 @@ describe('', () => { fireEvent.click(april); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2018, 3, 1, 12, 30)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 3, 1, 12, 30)); }); it('should not use `referenceDate` when a value is defined', () => { @@ -404,7 +404,7 @@ describe('', () => { fireEvent.click(april); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 1, 12, 20)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 1, 12, 20)); }); it('should not use `referenceDate` when a defaultValue is defined', () => { @@ -424,7 +424,7 @@ describe('', () => { fireEvent.click(april); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2019, 3, 1, 12, 20)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 3, 1, 12, 20)); }); }); @@ -454,7 +454,7 @@ describe('', () => { fireEvent.click(year2022); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 4, 1)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 4, 1)); }); it('should respect minDate when selecting closest enabled date', () => { @@ -474,7 +474,7 @@ describe('', () => { fireEvent.click(year2017); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2017, 4, 12)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2017, 4, 12)); }); it('should respect maxDate when selecting closest enabled date', () => { @@ -494,7 +494,7 @@ describe('', () => { fireEvent.click(year2022); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 2, 31)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 2, 31)); }); it('should go to next view without changing the date when no date of the new year is enabled', () => { @@ -559,7 +559,7 @@ describe('', () => { fireEvent.click(year2022); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 0, 1, 12, 30)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 0, 1, 12, 30)); }); it('should not use `referenceDate` when a value is defined', () => { @@ -579,7 +579,7 @@ describe('', () => { fireEvent.click(year2022); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 0, 1, 12, 20)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 0, 1, 12, 20)); }); it('should not use `referenceDate` when a defaultValue is defined', () => { @@ -599,7 +599,7 @@ describe('', () => { fireEvent.click(year2022); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.args[0]).toEqualDateTime(new Date(2022, 0, 1, 12, 20)); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2022, 0, 1, 12, 20)); }); }); diff --git a/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx b/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx index 589b88e5b47a..c18601db5c52 100644 --- a/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx +++ b/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx @@ -17,6 +17,7 @@ import { TimeView } from '../models'; import { DIGITAL_CLOCK_VIEW_HEIGHT } from '../internals/constants/dimensions'; import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; import { singleItemValueManager } from '../internals/utils/valueManagers'; +import { useClockReferenceDate } from '../internals/hooks/useClockReferenceDate'; const useUtilityClasses = (ownerState: DigitalClockProps) => { const { classes } = ownerState; @@ -104,6 +105,8 @@ export const DigitalClock = React.forwardRef(function DigitalClock handleRawValueChange(newValue, 'finish'), ); @@ -189,11 +199,6 @@ export const DigitalClock = React.forwardRef(function DigitalClock value || utils.setSeconds(utils.setMinutes(utils.setHours(now, 0), 0), 0), - [value, now, utils], - ); - const isTimeDisabled = React.useCallback( (valueToCheck: TDate) => { const isAfter = createIsAfterIgnoreDatePart(disableIgnoringDatePartForTimeValidation, utils); @@ -251,15 +256,15 @@ export const DigitalClock = React.forwardRef(function DigitalClock { - const startOfDay = utils.startOfDay(selectedTimeOrMidnight); + const startOfDay = utils.startOfDay(valueOrReferenceDate); return [ startOfDay, ...Array.from({ length: Math.ceil((24 * 60) / timeStep) - 1 }, (_, index) => utils.addMinutes(startOfDay, timeStep * (index + 1)), ), - utils.endOfDay(selectedTimeOrMidnight), + utils.endOfDay(valueOrReferenceDate), ]; - }, [selectedTimeOrMidnight, timeStep, utils]); + }, [valueOrReferenceDate, timeStep, utils]); return ( ', () => { + const { render } = createPickerRenderer(); + + describe('Reference date', () => { + it('should use `referenceDate` when no value defined', () => { + const onChange = spy(); + + render( + , + ); + + digitalClockHandler.setViewValue( + adapterToUse, + adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30), + ); + expect(onChange.callCount).to.equal(1); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 1, 15, 30)); + }); + + it('should not use `referenceDate` when a value is defined', () => { + const onChange = spy(); + + render( + , + ); + + digitalClockHandler.setViewValue( + adapterToUse, + adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30), + ); + expect(onChange.callCount).to.equal(1); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 30)); + }); + + it('should not use `referenceDate` when a defaultValue is defined', () => { + const onChange = spy(); + + render( + , + ); + + digitalClockHandler.setViewValue( + adapterToUse, + adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30), + ); + expect(onChange.callCount).to.equal(1); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 30)); + }); + }); +}); 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 ecc047d8aea4..a9fe62d31073 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 @@ -1,10 +1,11 @@ import * as React from 'react'; -import { screen, userEvent, describeConformance } from '@mui/monorepo/test/utils'; +import { screen, describeConformance } from '@mui/monorepo/test/utils'; import { describeValidation } from '@mui/x-date-pickers/tests/describeValidation'; import { describeValue } from '@mui/x-date-pickers/tests/describeValue'; import { createPickerRenderer, adapterToUse, wrapPickerMount } from 'test/utils/pickers-utils'; import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; import { expect } from 'chai'; +import { digitalClockHandler } from 'test/utils/pickers/viewHandlers'; describe(' - Describes', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); @@ -63,12 +64,7 @@ describe(' - Describes', () => { }, setNewValue: (value) => { const newValue = adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 30); - const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); - const formattedLabel = adapterToUse.format( - newValue, - hasMeridiem ? 'fullTime12h' : 'fullTime24h', - ); - userEvent.mousePress(screen.getByRole('option', { name: formattedLabel })); + digitalClockHandler.setViewValue(adapterToUse, newValue); return newValue; }, diff --git a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx index b2430bb5dadb..0256b464f91d 100644 --- a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx +++ b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx @@ -350,7 +350,7 @@ MonthCalendar.propTypes = { readOnly: PropTypes.bool, /** * The date used to generate the new value when both `value` and `defaultValue` are empty. - * @default The closest valid month using the validation props, except callbacks such as `shouldDisableDate`. + * @default The closest valid month using the validation props, except callbacks such as `shouldDisableMonth`. */ referenceDate: PropTypes.any, /** diff --git a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts index 6de34938e659..acbd25a21162 100644 --- a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts +++ b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts @@ -43,7 +43,7 @@ export interface MonthCalendarProps defaultValue?: TDate | null; /** * The date used to generate the new value when both `value` and `defaultValue` are empty. - * @default The closest valid month using the validation props, except callbacks such as `shouldDisableDate`. + * @default The closest valid month using the validation props, except callbacks such as `shouldDisableMonth`. */ referenceDate?: TDate; /** diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx index 88662a6423eb..3eccb3ad95d2 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx @@ -21,6 +21,7 @@ import { TimeStepOptions, TimeView } from '../models'; import { TimeViewWithMeridiem } from '../internals/models'; import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; import { singleItemValueManager } from '../internals/utils/valueManagers'; +import { useClockReferenceDate } from '../internals/hooks/useClockReferenceDate'; const useUtilityClasses = (ownerState: MultiSectionDigitalClockProps) => { const { classes } = ownerState; @@ -65,6 +66,8 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi slots, slotProps, value: valueProp, + defaultValue, + referenceDate: referenceDateProp, disableIgnoringDatePartForTimeValidation = false, maxTime, minTime, @@ -74,7 +77,6 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi shouldDisableClock, shouldDisableTime, onChange, - defaultValue, view: inView, views: inViews = ['hours', 'minutes'], openTo, @@ -115,6 +117,14 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi [inTimeSteps], ); + const valueOrReferenceDate = useClockReferenceDate({ + value, + referenceDate: referenceDateProp, + utils, + props, + timezone, + }); + const handleValueChange = useEventCallback( ( newValue: TDate | null, @@ -140,17 +150,12 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi onFocusedViewChange, }); - const selectedTimeOrMidnight = React.useMemo( - () => value || utils.setSeconds(utils.setMinutes(utils.setHours(now, 0), 0), 0), - [value, now, utils], - ); - const handleMeridiemValueChange = useEventCallback((newValue: TDate | null) => { setValueAndGoToView(newValue, null, 'meridiem'); }); const { meridiemMode, handleMeridiemChange } = useMeridiemMode( - selectedTimeOrMidnight, + valueOrReferenceDate, ampm, handleMeridiemValueChange, 'finish', @@ -194,16 +199,16 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi if (shouldDisableTime) { switch (viewType) { case 'hours': - return !shouldDisableTime(utils.setHours(selectedTimeOrMidnight, timeValue), 'hours'); + return !shouldDisableTime(utils.setHours(valueOrReferenceDate, timeValue), 'hours'); case 'minutes': return !shouldDisableTime( - utils.setMinutes(selectedTimeOrMidnight, timeValue), + utils.setMinutes(valueOrReferenceDate, timeValue), 'minutes', ); case 'seconds': return !shouldDisableTime( - utils.setSeconds(selectedTimeOrMidnight, timeValue), + utils.setSeconds(valueOrReferenceDate, timeValue), 'seconds', ); @@ -218,7 +223,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi switch (viewType) { case 'hours': { const valueWithMeridiem = convertValueToMeridiem(rawValue, meridiemMode, ampm); - const dateWithNewHours = utils.setHours(selectedTimeOrMidnight, valueWithMeridiem); + const dateWithNewHours = utils.setHours(valueOrReferenceDate, valueWithMeridiem); const start = utils.setSeconds(utils.setMinutes(dateWithNewHours, 0), 0); const end = utils.setSeconds(utils.setMinutes(dateWithNewHours, 59), 59); @@ -226,7 +231,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi } case 'minutes': { - const dateWithNewMinutes = utils.setMinutes(selectedTimeOrMidnight, rawValue); + const dateWithNewMinutes = utils.setMinutes(valueOrReferenceDate, rawValue); const start = utils.setSeconds(dateWithNewMinutes, 0); const end = utils.setSeconds(dateWithNewMinutes, 59); @@ -234,7 +239,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi } case 'seconds': { - const dateWithNewSeconds = utils.setSeconds(selectedTimeOrMidnight, rawValue); + const dateWithNewSeconds = utils.setSeconds(valueOrReferenceDate, rawValue); const start = dateWithNewSeconds; const end = dateWithNewSeconds; @@ -247,7 +252,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi }, [ ampm, - selectedTimeOrMidnight, + valueOrReferenceDate, disableIgnoringDatePartForTimeValidation, maxTime, meridiemMode, @@ -278,10 +283,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi return { onChange: (hours) => { const valueWithMeridiem = convertValueToMeridiem(hours, meridiemMode, ampm); - handleSectionChange( - 'hours', - utils.setHours(selectedTimeOrMidnight, valueWithMeridiem), - ); + handleSectionChange('hours', utils.setHours(valueOrReferenceDate, valueWithMeridiem)); }, items: getHourSectionOptions({ now, @@ -298,10 +300,10 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi case 'minutes': { return { onChange: (minutes) => { - handleSectionChange('minutes', utils.setMinutes(selectedTimeOrMidnight, minutes)); + handleSectionChange('minutes', utils.setMinutes(valueOrReferenceDate, minutes)); }, items: getTimeSectionOptions({ - value: utils.getMinutes(selectedTimeOrMidnight), + value: utils.getMinutes(valueOrReferenceDate), isDisabled: (minutes) => disabled || isTimeDisabled(minutes, 'minutes'), resolveLabel: (minutes) => utils.format(utils.setMinutes(now, minutes), 'minutes'), timeStep: timeSteps.minutes, @@ -314,10 +316,10 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi case 'seconds': { return { onChange: (seconds) => { - handleSectionChange('seconds', utils.setSeconds(selectedTimeOrMidnight, seconds)); + handleSectionChange('seconds', utils.setSeconds(valueOrReferenceDate, seconds)); }, items: getTimeSectionOptions({ - value: utils.getSeconds(selectedTimeOrMidnight), + value: utils.getSeconds(valueOrReferenceDate), isDisabled: (seconds) => disabled || isTimeDisabled(seconds, 'seconds'), resolveLabel: (seconds) => utils.format(utils.setSeconds(now, seconds), 'seconds'), timeStep: timeSteps.seconds, @@ -366,7 +368,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi localeText.secondsClockNumberText, meridiemMode, handleSectionChange, - selectedTimeOrMidnight, + valueOrReferenceDate, disabled, isTimeDisabled, handleMeridiemChange, @@ -489,7 +491,7 @@ MultiSectionDigitalClock.propTypes = { minutesStep: PropTypes.number, /** * Callback fired when the value changes. - * @template TDate + * @template TDate, TView * @param {TDate | null} value The new value. * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. * @param {TView | undefined} selectedView Indicates the view in which the selection has been made. @@ -519,6 +521,11 @@ MultiSectionDigitalClock.propTypes = { * @default false */ readOnly: PropTypes.bool, + /** + * The date used to generate the new value when both `value` and `defaultValue` are empty. + * @default The closest valid time using the validation props, except callbacks such as `shouldDisableTime`. + */ + referenceDate: PropTypes.any, /** * Disable specific clock time. * @param {number} clockValue The value to check. diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/MultiSectionDigitalClock.test.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/MultiSectionDigitalClock.test.tsx new file mode 100644 index 000000000000..0306f761d54a --- /dev/null +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/tests/MultiSectionDigitalClock.test.tsx @@ -0,0 +1,86 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { + MultiSectionDigitalClock, + MultiSectionDigitalClockProps, +} from '@mui/x-date-pickers/MultiSectionDigitalClock'; +import { adapterToUse, createPickerRenderer } from 'test/utils/pickers-utils'; +import { multiSectionDigitalClockHandler } from 'test/utils/pickers/viewHandlers'; + +describe('', () => { + const { render } = createPickerRenderer(); + + describe('Reference date', () => { + it('should use `referenceDate` when no value defined', () => { + const onChange = spy(); + + render( + , + ); + + multiSectionDigitalClockHandler.setViewValue( + adapterToUse, + adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30), + ); + expect(onChange.callCount).to.equal(3); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 1, 15, 30)); + }); + + it('should not use `referenceDate` when a value is defined', () => { + const onChange = spy(); + + function ControlledMultiSectionDigitalClock(props: MultiSectionDigitalClockProps) { + const [value, setValue] = React.useState(props.value); + + return ( + { + setValue(newValue); + props.onChange?.(newValue); + }} + /> + ); + } + + render( + , + ); + + multiSectionDigitalClockHandler.setViewValue( + adapterToUse, + adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30), + ); + expect(onChange.callCount).to.equal(3); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 30)); + }); + + it('should not use `referenceDate` when a defaultValue is defined', () => { + const onChange = spy(); + + render( + , + ); + + multiSectionDigitalClockHandler.setViewValue( + adapterToUse, + adapterToUse.setMinutes(adapterToUse.setHours(adapterToUse.date(), 15), 30), + ); + expect(onChange.callCount).to.equal(3); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 30)); + }); + }); +}); 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 205470e0d858..764e2690a3a6 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 @@ -1,10 +1,11 @@ import * as React from 'react'; -import { screen, userEvent, describeConformance } from '@mui/monorepo/test/utils'; +import { screen, describeConformance } from '@mui/monorepo/test/utils'; import { describeValidation } from '@mui/x-date-pickers/tests/describeValidation'; import { describeValue } from '@mui/x-date-pickers/tests/describeValue'; import { createPickerRenderer, adapterToUse, wrapPickerMount } from 'test/utils/pickers-utils'; import { MultiSectionDigitalClock } from '@mui/x-date-pickers/MultiSectionDigitalClock'; import { expect } from 'chai'; +import { multiSectionDigitalClockHandler } from 'test/utils/pickers/viewHandlers'; describe(' - Describes', () => { const { render, clock } = createPickerRenderer({ clock: 'fake' }); @@ -69,21 +70,7 @@ describe(' - Describes', () => { }, setNewValue: (value) => { const newValue = adapterToUse.addMinutes(adapterToUse.addHours(value, 1), 5); - const hasMeridiem = adapterToUse.is12HourCycleInCurrentLocale(); - const hoursLabel = parseInt( - adapterToUse.format(newValue, hasMeridiem ? 'hours12h' : 'hours24h'), - 10, - ); - 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(newValue) >= 12 ? 'pm' : 'am'), - }), - ); - } + multiSectionDigitalClockHandler.setViewValue(adapterToUse, newValue); return newValue; }, diff --git a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx index b39556d5617d..d450f0e1d4e4 100644 --- a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx +++ b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx @@ -18,6 +18,7 @@ import { getHourNumbers, getMinutesNumbers } from './ClockNumbers'; import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { uncapitalizeObjectKeys } from '../internals/utils/slots-migration'; +import { useClockReferenceDate } from '../internals/hooks/useClockReferenceDate'; const useUtilityClasses = (ownerState: TimeClockProps) => { const { classes } = ownerState; @@ -53,6 +54,8 @@ type TimeClockComponent = (( props: TimeClockProps & React.RefAttributes, ) => JSX.Element) & { propTypes?: any }; +const TIME_CLOCK_DEFAULT_VIEWS: TimeView[] = ['hours', 'minutes']; + /** * * API: @@ -79,6 +82,8 @@ export const TimeClock = React.forwardRef(function TimeClock(); const now = useNow(timezone); @@ -127,13 +140,8 @@ export const TimeClock = React.forwardRef(function TimeClock value || utils.setSeconds(utils.setMinutes(utils.setHours(now, 0), 0), 0), - [value, now, utils], - ); - const { meridiemMode, handleMeridiemChange } = useMeridiemMode( - selectedTimeOrMidnight, + valueOrReferenceDate, ampm, setValueAndGoToNextView, ); @@ -176,16 +184,16 @@ export const TimeClock = React.forwardRef(function TimeClock { const valueWithMeridiem = convertValueToMeridiem(hourValue, meridiemMode, ampm); setValueAndGoToNextView( - utils.setHours(selectedTimeOrMidnight, valueWithMeridiem), + utils.setHours(valueOrReferenceDate, valueWithMeridiem), isFinish, ); }; return { onChange: handleHoursChange, - viewValue: utils.getHours(selectedTimeOrMidnight), + viewValue: utils.getHours(valueOrReferenceDate), children: getHourNumbers({ value, utils, @@ -276,9 +284,9 @@ export const TimeClock = React.forwardRef(function TimeClock { - setValueAndGoToNextView(utils.setMinutes(selectedTimeOrMidnight, minuteValue), isFinish); + setValueAndGoToNextView(utils.setMinutes(valueOrReferenceDate, minuteValue), isFinish); }; return { @@ -296,9 +304,9 @@ export const TimeClock = React.forwardRef(function TimeClock { - setValueAndGoToNextView(utils.setSeconds(selectedTimeOrMidnight, secondValue), isFinish); + setValueAndGoToNextView(utils.setSeconds(valueOrReferenceDate, secondValue), isFinish); }; return { @@ -328,7 +336,7 @@ export const TimeClock = React.forwardRef(function TimeClock', () => { const { render } = createPickerRenderer(); @@ -514,4 +515,65 @@ describe('', () => { expect(adapterToUse.getSeconds(newDate)).to.equal(0); }); }); + + describe('Reference date', () => { + it('should use `referenceDate` when no value defined', () => { + const onChange = spy(); + + render( + , + ); + + timeClockHandler.setViewValue( + adapterToUse, + adapterToUse.setHours(adapterToUse.date(), 3), + 'hours', + ); + expect(onChange.callCount).to.equal(2); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2018, 0, 1, 15, 30)); + }); + + it('should not use `referenceDate` when a value is defined', () => { + const onChange = spy(); + + render( + , + ); + + timeClockHandler.setViewValue( + adapterToUse, + adapterToUse.setHours(adapterToUse.date(), 3), + 'hours', + ); + expect(onChange.callCount).to.equal(2); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 20)); + }); + + it('should not use `referenceDate` when a defaultValue is defined', () => { + const onChange = spy(); + + render( + , + ); + + timeClockHandler.setViewValue( + adapterToUse, + adapterToUse.setHours(adapterToUse.date(), 3), + 'hours', + ); + expect(onChange.callCount).to.equal(2); + expect(onChange.lastCall.firstArg).toEqualDateTime(new Date(2019, 0, 1, 15, 20)); + }); + }); }); 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 12fb9ce5cf07..b26813026831 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 @@ -1,18 +1,14 @@ import * as React from 'react'; import { expect } from 'chai'; -import { describeConformance, fireTouchChangedEvent, screen } from '@mui/monorepo/test/utils'; +import { describeConformance, screen } from '@mui/monorepo/test/utils'; import { describeValue } from '@mui/x-date-pickers/tests/describeValue'; import { clockPointerClasses, TimeClock, timeClockClasses as classes, } from '@mui/x-date-pickers/TimeClock'; -import { - adapterToUse, - wrapPickerMount, - createPickerRenderer, - getClockTouchEvent, -} from 'test/utils/pickers-utils'; +import { adapterToUse, wrapPickerMount, createPickerRenderer } from 'test/utils/pickers-utils'; +import { timeClockHandler } from 'test/utils/pickers/viewHandlers'; describe(' - Describes', () => { const { render, clock } = createPickerRenderer(); @@ -56,18 +52,9 @@ describe(' - Describes', () => { }, setNewValue: (value) => { 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); + + timeClockHandler.setViewValue(adapterToUse, newValue, 'hours'); + timeClockHandler.setViewValue(adapterToUse, newValue, 'minutes'); return newValue; }, diff --git a/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx b/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx index 667103c8fcf2..4ec4983134e9 100644 --- a/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx +++ b/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx @@ -363,7 +363,7 @@ YearCalendar.propTypes = { readOnly: PropTypes.bool, /** * The date used to generate the new value when both `value` and `defaultValue` are empty. - * @default The closest valid year using the validation props, except callbacks such as `shouldDisableDate`. + * @default The closest valid year using the validation props, except callbacks such as `shouldDisableYear`. */ referenceDate: PropTypes.any, /** diff --git a/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts b/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts index 12dd8c8dae5f..aa02ff57b060 100644 --- a/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts +++ b/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts @@ -44,7 +44,7 @@ export interface YearCalendarProps defaultValue?: TDate | null; /** * The date used to generate the new value when both `value` and `defaultValue` are empty. - * @default The closest valid year using the validation props, except callbacks such as `shouldDisableDate`. + * @default The closest valid year using the validation props, except callbacks such as `shouldDisableYear`. */ referenceDate?: TDate; /** diff --git a/packages/x-date-pickers/src/internals/hooks/useClockReferenceDate.ts b/packages/x-date-pickers/src/internals/hooks/useClockReferenceDate.ts new file mode 100644 index 000000000000..f7f4062bad21 --- /dev/null +++ b/packages/x-date-pickers/src/internals/hooks/useClockReferenceDate.ts @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { MuiPickersAdapter, PickersTimezone } from '../../models'; +import { singleItemValueManager } from '../utils/valueManagers'; +import { getTodayDate } from '../utils/date-utils'; +import { SECTION_TYPE_GRANULARITY } from '../utils/getDefaultReferenceDate'; + +export const useClockReferenceDate = ({ + value, + referenceDate: referenceDateProp, + utils, + props, + timezone, +}: { + value: TDate; + referenceDate: TDate | undefined; + utils: MuiPickersAdapter; + props: TProps; + timezone: PickersTimezone; +}) => { + const referenceDate = React.useMemo( + () => + singleItemValueManager.getInitialReferenceValue({ + value, + utils, + props, + referenceDate: referenceDateProp, + granularity: SECTION_TYPE_GRANULARITY.day, + timezone, + getTodayDate: () => getTodayDate(utils, timezone, 'date'), + }), // We only want to compute the reference date on mount. + [], // eslint-disable-line react-hooks/exhaustive-deps + ); + + return value ?? referenceDate; +}; diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts index 30201d35b42d..93510ad80c05 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.types.ts @@ -54,6 +54,7 @@ export interface PickerValueManager { * @param {MuiPickersAdapter} params.utils The adapter. * @param {number} params.granularity The granularity of the selection possible on this component. * @param {PickersTimezone} params.timezone The current timezone. + * @param {() => TDate} params.getTodayDate The reference date to use if no reference date is passed to the component. * @returns {TValue} The reference value to use for non-provided dates. */ getInitialReferenceValue: (params: { @@ -63,6 +64,7 @@ export interface PickerValueManager { utils: MuiPickersAdapter; granularity: number; timezone: PickersTimezone; + getTodayDate?: () => TDate; }) => TValue; /** * Method parsing the input value to replace all invalid dates by `null`. diff --git a/packages/x-date-pickers/src/internals/models/props/clock.ts b/packages/x-date-pickers/src/internals/models/props/clock.ts index 3acc0e347929..4fee82c002da 100644 --- a/packages/x-date-pickers/src/internals/models/props/clock.ts +++ b/packages/x-date-pickers/src/internals/models/props/clock.ts @@ -38,7 +38,7 @@ export interface BaseClockProps defaultValue?: TDate | null; /** * Callback fired when the value changes. - * @template TDate + * @template TDate, TView * @param {TDate | null} value The new value. * @param {PickerSelectionState | undefined} selectionState Indicates if the date selection is complete. * @param {TView | undefined} selectedView Indicates the view in which the selection has been made. @@ -58,6 +58,11 @@ export interface BaseClockProps * @default false */ readOnly?: boolean; + /** + * The date used to generate the new value when both `value` and `defaultValue` are empty. + * @default The closest valid time using the validation props, except callbacks such as `shouldDisableTime`. + */ + referenceDate?: TDate; } export interface DesktopOnlyTimePickerProps diff --git a/packages/x-date-pickers/src/internals/utils/getDefaultReferenceDate.ts b/packages/x-date-pickers/src/internals/utils/getDefaultReferenceDate.ts index 28c10127da31..5d37db6e4aa9 100644 --- a/packages/x-date-pickers/src/internals/utils/getDefaultReferenceDate.ts +++ b/packages/x-date-pickers/src/internals/utils/getDefaultReferenceDate.ts @@ -1,6 +1,6 @@ import { createIsAfterIgnoreDatePart } from './time-utils'; import { mergeDateAndTime, getTodayDate } from './date-utils'; -import { FieldSection, MuiPickersAdapter, PickersTimezone } from '../../models'; +import { DateOrTimeView, FieldSection, MuiPickersAdapter, PickersTimezone } from '../../models'; export interface GetDefaultReferenceDateProps { maxDate?: TDate; @@ -28,6 +28,9 @@ export const getSectionTypeGranularity = (sections: FieldSection[]) => ), ); +export const getViewsGranularity = (views: readonly DateOrTimeView[]) => + Math.max(...views.map((view) => SECTION_TYPE_GRANULARITY[view] ?? 1)); + const roundDate = (utils: MuiPickersAdapter, granularity: number, date: TDate) => { if (granularity === SECTION_TYPE_GRANULARITY.year) { return utils.startOfYear(date); @@ -59,13 +62,17 @@ export const getDefaultReferenceDate = ({ utils, granularity, timezone, + getTodayDate: inGetTodayDate, }: { props: GetDefaultReferenceDateProps; utils: MuiPickersAdapter; granularity: number; timezone: PickersTimezone; + getTodayDate?: () => TDate; }) => { - let referenceDate = roundDate(utils, granularity, getTodayDate(utils, timezone)); + let referenceDate = inGetTodayDate + ? inGetTodayDate() + : roundDate(utils, granularity, getTodayDate(utils, timezone)); if (props.minDate != null && utils.isAfterDay(props.minDate, referenceDate)) { referenceDate = roundDate(utils, granularity, props.minDate); diff --git a/test/utils/pickers-utils.tsx b/test/utils/pickers-utils.tsx index cef275cf578f..3fcb05118ce7 100644 --- a/test/utils/pickers-utils.tsx +++ b/test/utils/pickers-utils.tsx @@ -211,7 +211,10 @@ export const getClockMouseEvent = ( return event; }; -export const getClockTouchEvent = (value: number, view: 'minutes' | '12hours' | '24hours') => { +export const getClockTouchEvent = ( + value: number | string, + view: 'minutes' | '12hours' | '24hours', +) => { // TODO: Handle 24 hours clock if (view === '24hours') { throw new Error('Do not support 24 hours clock yet'); @@ -224,7 +227,7 @@ export const getClockTouchEvent = (value: number, view: 'minutes' | '12hours' | itemCount = 12; } - const angle = Math.PI / 2 - (Math.PI * 2 * value) / itemCount; + const angle = Math.PI / 2 - (Math.PI * 2 * Number(value)) / itemCount; const clientX = Math.round(((1 + Math.cos(angle)) * CLOCK_WIDTH) / 2); const clientY = Math.round(((1 - Math.sin(angle)) * CLOCK_WIDTH) / 2); diff --git a/test/utils/pickers/viewHandlers.ts b/test/utils/pickers/viewHandlers.ts new file mode 100644 index 000000000000..9ec3e39d2fbd --- /dev/null +++ b/test/utils/pickers/viewHandlers.ts @@ -0,0 +1,58 @@ +import { fireTouchChangedEvent, userEvent, screen } from '@mui/monorepo/test/utils'; +import { getClockTouchEvent } from 'test/utils/pickers-utils'; +import { MuiPickersAdapter, TimeView } from '@mui/x-date-pickers/models'; + +type TDate = any; + +interface ViewHandler { + setViewValue: (utils: MuiPickersAdapter, viewValue: TDate, view?: TView) => void; +} + +export const timeClockHandler: ViewHandler = { + setViewValue: (adapter, value, view) => { + const hasMeridiem = adapter.is12HourCycleInCurrentLocale(); + + let valueInt; + let clockView; + + if (view === 'hours') { + valueInt = adapter.getHours(value); + clockView = hasMeridiem ? '12hours' : '24hours'; + } else if (view === 'minutes') { + valueInt = adapter.getMinutes(value); + clockView = 'minutes'; + } else { + throw new Error('View not supported'); + } + + const hourClockEvent = getClockTouchEvent(valueInt, clockView); + + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', hourClockEvent); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', hourClockEvent); + }, +}; + +export const digitalClockHandler: ViewHandler = { + setViewValue: (adapter, value) => { + const hasMeridiem = adapter.is12HourCycleInCurrentLocale(); + const formattedLabel = adapter.format(value, hasMeridiem ? 'fullTime12h' : 'fullTime24h'); + userEvent.mousePress(screen.getByRole('option', { name: formattedLabel })); + }, +}; + +export const multiSectionDigitalClockHandler: ViewHandler = { + setViewValue: (adapter, value) => { + const hasMeridiem = adapter.is12HourCycleInCurrentLocale(); + const hoursLabel = parseInt(adapter.format(value, hasMeridiem ? 'hours12h' : 'hours24h'), 10); + const minutesLabel = adapter.getMinutes(value).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: adapter.getMeridiemText(adapter.getHours(value) >= 12 ? 'pm' : 'am'), + }), + ); + } + }, +};