From 13658f04f74d4e693b3ed5d9cb0408c15675ece6 Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Tue, 20 Jun 2023 09:56:23 +0200 Subject: [PATCH] [pickers] Add proper support for UTC and timezones (#8261) --- .../adapters-locale/adapters-locale.md | 71 ---- .../timezone/BasicTimezoneProp.js | 38 ++ .../timezone/BasicTimezoneProp.tsx | 40 ++ .../timezone/BasicTimezoneProp.tsx.preview | 15 + .../date-pickers/timezone/BasicValueProp.js | 33 ++ .../date-pickers/timezone/BasicValueProp.tsx | 33 ++ .../timezone/BasicValueProp.tsx.preview | 8 + .../date-pickers/timezone/DayjsTimezone.js | 29 ++ .../date-pickers/timezone/DayjsTimezone.tsx | 29 ++ .../timezone/DayjsTimezone.tsx.preview | 4 + docs/data/date-pickers/timezone/DayjsUTC.js | 25 ++ docs/data/date-pickers/timezone/DayjsUTC.tsx | 27 ++ .../timezone/DayjsUTC.tsx.preview | 4 + .../date-pickers/timezone/LuxonTimezone.js | 24 ++ .../date-pickers/timezone/LuxonTimezone.tsx | 24 ++ .../timezone/LuxonTimezone.tsx.preview | 4 + docs/data/date-pickers/timezone/LuxonUTC.js | 24 ++ docs/data/date-pickers/timezone/LuxonUTC.tsx | 24 ++ .../timezone/LuxonUTC.tsx.preview | 4 + .../date-pickers/timezone/MomentTimezone.js | 24 ++ .../date-pickers/timezone/MomentTimezone.tsx | 24 ++ .../timezone/MomentTimezone.tsx.preview | 4 + docs/data/date-pickers/timezone/MomentUTC.js | 22 + docs/data/date-pickers/timezone/MomentUTC.tsx | 24 ++ .../timezone/MomentUTC.tsx.preview | 4 + .../StoreUTCButDisplayOtherTimezone.js | 31 ++ .../StoreUTCButDisplayOtherTimezone.tsx | 33 ++ ...toreUTCButDisplayOtherTimezone.tsx.preview | 8 + .../StoreUTCButDisplaySystemTimezone.js | 27 ++ .../StoreUTCButDisplaySystemTimezone.tsx | 29 ++ ...oreUTCButDisplaySystemTimezone.tsx.preview | 4 + .../timezone/TimezonePlayground.js | 55 +++ .../timezone/TimezonePlayground.tsx | 57 +++ docs/data/date-pickers/timezone/timezone.md | 383 ++++++++++++++++++ docs/data/pages.ts | 4 + docs/package.json | 1 + .../x/api/date-pickers/date-calendar.json | 4 + docs/pages/x/api/date-pickers/date-field.json | 4 + .../pages/x/api/date-pickers/date-picker.json | 4 + .../api/date-pickers/date-range-calendar.json | 4 + .../x/api/date-pickers/date-range-picker.json | 4 + .../x/api/date-pickers/date-time-field.json | 4 + .../x/api/date-pickers/date-time-picker.json | 4 + .../api/date-pickers/desktop-date-picker.json | 4 + .../desktop-date-range-picker.json | 4 + .../desktop-date-time-picker.json | 4 + .../api/date-pickers/desktop-time-picker.json | 4 + .../x/api/date-pickers/digital-clock.json | 4 + .../date-pickers/localization-provider.json | 2 +- .../api/date-pickers/mobile-date-picker.json | 4 + .../mobile-date-range-picker.json | 4 + .../date-pickers/mobile-date-time-picker.json | 4 + .../api/date-pickers/mobile-time-picker.json | 4 + .../x/api/date-pickers/month-calendar.json | 4 + .../multi-input-date-range-field.json | 4 + .../multi-input-date-time-range-field.json | 4 + .../multi-input-time-range-field.json | 4 + .../multi-section-digital-clock.json | 4 + .../single-input-date-range-field.json | 4 + .../single-input-date-time-range-field.json | 4 + .../single-input-time-range-field.json | 4 + .../api/date-pickers/static-date-picker.json | 4 + .../static-date-range-picker.json | 4 + .../date-pickers/static-date-time-picker.json | 4 + .../api/date-pickers/static-time-picker.json | 4 + docs/pages/x/api/date-pickers/time-clock.json | 4 + docs/pages/x/api/date-pickers/time-field.json | 4 + .../pages/x/api/date-pickers/time-picker.json | 4 + .../x/api/date-pickers/year-calendar.json | 4 + docs/pages/x/react-date-pickers/timezone.js | 7 + .../api-docs/date-pickers/date-calendar.json | 1 + .../api-docs/date-pickers/date-field.json | 1 + .../api-docs/date-pickers/date-picker.json | 1 + .../date-pickers/date-range-calendar.json | 1 + .../date-pickers/date-range-picker.json | 1 + .../date-pickers/date-time-field.json | 1 + .../date-pickers/date-time-picker.json | 1 + .../date-pickers/desktop-date-picker.json | 1 + .../desktop-date-range-picker.json | 1 + .../desktop-date-time-picker.json | 1 + .../date-pickers/desktop-time-picker.json | 1 + .../api-docs/date-pickers/digital-clock.json | 1 + .../date-pickers/mobile-date-picker.json | 1 + .../mobile-date-range-picker.json | 1 + .../date-pickers/mobile-date-time-picker.json | 1 + .../date-pickers/mobile-time-picker.json | 1 + .../api-docs/date-pickers/month-calendar.json | 3 +- .../multi-input-date-range-field.json | 1 + .../multi-input-date-time-range-field.json | 1 + .../multi-input-time-range-field.json | 1 + .../multi-section-digital-clock.json | 1 + .../single-input-date-range-field.json | 1 + .../single-input-date-time-range-field.json | 1 + .../single-input-time-range-field.json | 1 + .../date-pickers/static-date-picker.json | 1 + .../static-date-range-picker.json | 1 + .../date-pickers/static-date-time-picker.json | 1 + .../date-pickers/static-time-picker.json | 1 + .../api-docs/date-pickers/time-clock.json | 1 + .../api-docs/date-pickers/time-field.json | 1 + .../api-docs/date-pickers/time-picker.json | 1 + .../api-docs/date-pickers/year-calendar.json | 3 +- .../DateRangeCalendar/DateRangeCalendar.tsx | 44 +- .../DateRangeCalendar.types.ts | 2 + .../src/DateRangeCalendar/useDragRange.ts | 26 +- .../src/DateRangePicker/DateRangePicker.tsx | 8 + .../DesktopDateRangePicker.tsx | 8 + .../MobileDateRangePicker.tsx | 8 + .../MultiInputDateRangeField.tsx | 8 + .../MultiInputDateTimeRangeField.tsx | 8 + .../MultiInputDateTimeRangeField.types.ts | 3 +- .../MultiInputTimeRangeField.tsx | 8 + .../MultiInputTimeRangeField.types.ts | 3 +- .../SingleInputDateRangeField.tsx | 8 + .../SingleInputDateRangeField.types.ts | 3 +- .../SingleInputDateTimeRangeField.tsx | 8 + .../SingleInputTimeRangeField.tsx | 8 + .../StaticDateRangePicker.tsx | 8 + .../dateRangeViewRenderers.tsx | 2 + .../useDesktopRangePicker.tsx | 2 + .../useMobileRangePicker.tsx | 2 + .../useMultiInputDateRangeField.ts | 25 +- .../useMultiInputDateTimeRangeField.ts | 27 +- .../useMultiInputTimeRangeField.ts | 27 +- .../utils/validation/validateDateRange.ts | 11 +- .../utils/validation/validateDateTimeRange.ts | 5 +- .../utils/validation/validateTimeRange.ts | 12 +- .../src/internal/utils/valueManagers.ts | 10 +- .../src/AdapterDayjs/AdapterDayjs.test.tsx | 17 +- .../src/AdapterDayjs/AdapterDayjs.ts | 66 ++- .../src/AdapterLuxon/AdapterLuxon.test.tsx | 3 +- .../src/AdapterLuxon/AdapterLuxon.ts | 20 +- .../src/AdapterMoment/AdapterMoment.test.tsx | 14 +- .../src/AdapterMoment/AdapterMoment.ts | 24 +- .../src/DateCalendar/DateCalendar.tsx | 35 +- .../src/DateCalendar/DateCalendar.types.ts | 5 +- .../src/DateCalendar/DayCalendar.tsx | 43 +- .../DateCalendar/PickersCalendarHeader.tsx | 3 + .../tests/timezone.DateCalendar.test.tsx | 66 +++ .../src/DateCalendar/useCalendarState.tsx | 8 +- .../src/DateCalendar/useIsDateDisabled.ts | 3 + .../src/DateField/DateField.tsx | 8 + .../src/DatePicker/DatePicker.tsx | 8 + .../src/DateTimeField/DateTimeField.tsx | 8 + .../tests/timezone.DateTimeField.test.tsx | 111 ++++- .../src/DateTimePicker/DateTimePicker.tsx | 8 + .../DesktopDatePicker/DesktopDatePicker.tsx | 8 + .../DesktopDateTimePicker.tsx | 8 + .../DesktopTimePicker/DesktopTimePicker.tsx | 8 + .../src/DigitalClock/DigitalClock.tsx | 45 +- .../tests/timezone.DigitalClock.test.tsx | 96 +++++ .../src/MobileDatePicker/MobileDatePicker.tsx | 8 + .../MobileDateTimePicker.tsx | 8 + .../src/MobileTimePicker/MobileTimePicker.tsx | 8 + .../src/MonthCalendar/MonthCalendar.tsx | 41 +- .../src/MonthCalendar/MonthCalendar.types.ts | 6 +- .../MultiSectionDigitalClock.tsx | 43 +- .../src/StaticDatePicker/StaticDatePicker.tsx | 8 + .../StaticDateTimePicker.tsx | 8 + .../src/StaticTimePicker/StaticTimePicker.tsx | 8 + .../src/TimeClock/TimeClock.tsx | 41 +- .../tests/timezone.TimeClock.test.tsx | 98 +++++ .../src/TimeField/TimeField.tsx | 8 + .../src/TimePicker/TimePicker.tsx | 8 + .../src/YearCalendar/YearCalendar.tsx | 40 +- .../src/YearCalendar/YearCalendar.types.ts | 6 +- .../dateTimeViewRenderers.tsx | 3 + .../dateViewRenderers/dateViewRenderers.tsx | 2 + .../internals/hooks/date-helpers-hooks.tsx | 22 +- .../useDesktopPicker/useDesktopPicker.tsx | 2 + .../src/internals/hooks/useField/useField.ts | 6 +- .../hooks/useField/useField.types.ts | 12 +- .../hooks/useField/useField.utils.ts | 80 ++-- .../useField/useFieldCharacterEditing.ts | 10 +- .../internals/hooks/useField/useFieldState.ts | 53 ++- .../hooks/useMobilePicker/useMobilePicker.tsx | 2 + .../hooks/usePicker/usePickerValue.ts | 23 +- .../hooks/usePicker/usePickerValue.types.ts | 29 +- .../hooks/usePicker/usePickerViews.ts | 7 +- .../src/internals/hooks/useUtils.ts | 9 +- .../internals/hooks/useValueWithTimezone.ts | 100 +++++ .../x-date-pickers/src/internals/index.ts | 1 + .../src/internals/models/props/clock.ts | 5 +- .../src/internals/utils/date-utils.test.ts | 12 + .../src/internals/utils/date-utils.ts | 16 +- .../src/internals/utils/fields.ts | 1 + .../utils/getDefaultReferenceDate.ts | 6 +- .../utils/validation/validateDate.ts | 27 +- .../utils/validation/validateTime.ts | 22 +- .../src/internals/utils/valueManagers.ts | 2 + .../x-date-pickers/src/models/adapters.ts | 4 +- .../x-date-pickers/src/models/timezone.ts | 11 + .../describeAdapters/describeAdapters.ts | 2 + .../describeGregorianAdapter.types.ts | 1 + .../testCalculations.ts | 105 +++-- .../describeJalaliAdapter/testCalculations.ts | 28 ++ .../timeViewRenderers/timeViewRenderers.tsx | 6 + scripts/x-date-pickers-pro.exports.json | 1 + scripts/x-date-pickers.exports.json | 1 + test/utils/pickers-utils.tsx | 24 ++ yarn.lock | 7 + 201 files changed, 2823 insertions(+), 424 deletions(-) create mode 100644 docs/data/date-pickers/timezone/BasicTimezoneProp.js create mode 100644 docs/data/date-pickers/timezone/BasicTimezoneProp.tsx create mode 100644 docs/data/date-pickers/timezone/BasicTimezoneProp.tsx.preview create mode 100644 docs/data/date-pickers/timezone/BasicValueProp.js create mode 100644 docs/data/date-pickers/timezone/BasicValueProp.tsx create mode 100644 docs/data/date-pickers/timezone/BasicValueProp.tsx.preview create mode 100644 docs/data/date-pickers/timezone/DayjsTimezone.js create mode 100644 docs/data/date-pickers/timezone/DayjsTimezone.tsx create mode 100644 docs/data/date-pickers/timezone/DayjsTimezone.tsx.preview create mode 100644 docs/data/date-pickers/timezone/DayjsUTC.js create mode 100644 docs/data/date-pickers/timezone/DayjsUTC.tsx create mode 100644 docs/data/date-pickers/timezone/DayjsUTC.tsx.preview create mode 100644 docs/data/date-pickers/timezone/LuxonTimezone.js create mode 100644 docs/data/date-pickers/timezone/LuxonTimezone.tsx create mode 100644 docs/data/date-pickers/timezone/LuxonTimezone.tsx.preview create mode 100644 docs/data/date-pickers/timezone/LuxonUTC.js create mode 100644 docs/data/date-pickers/timezone/LuxonUTC.tsx create mode 100644 docs/data/date-pickers/timezone/LuxonUTC.tsx.preview create mode 100644 docs/data/date-pickers/timezone/MomentTimezone.js create mode 100644 docs/data/date-pickers/timezone/MomentTimezone.tsx create mode 100644 docs/data/date-pickers/timezone/MomentTimezone.tsx.preview create mode 100644 docs/data/date-pickers/timezone/MomentUTC.js create mode 100644 docs/data/date-pickers/timezone/MomentUTC.tsx create mode 100644 docs/data/date-pickers/timezone/MomentUTC.tsx.preview create mode 100644 docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.js create mode 100644 docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx create mode 100644 docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx.preview create mode 100644 docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.js create mode 100644 docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx create mode 100644 docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx.preview create mode 100644 docs/data/date-pickers/timezone/TimezonePlayground.js create mode 100644 docs/data/date-pickers/timezone/TimezonePlayground.tsx create mode 100644 docs/data/date-pickers/timezone/timezone.md create mode 100644 docs/pages/x/react-date-pickers/timezone.js create mode 100644 packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx create mode 100644 packages/x-date-pickers/src/DigitalClock/tests/timezone.DigitalClock.test.tsx create mode 100644 packages/x-date-pickers/src/TimeClock/tests/timezone.TimeClock.test.tsx create mode 100644 packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts diff --git a/docs/data/date-pickers/adapters-locale/adapters-locale.md b/docs/data/date-pickers/adapters-locale/adapters-locale.md index 291dcd11b034e..04e5526934c03 100644 --- a/docs/data/date-pickers/adapters-locale/adapters-locale.md +++ b/docs/data/date-pickers/adapters-locale/adapters-locale.md @@ -250,74 +250,3 @@ This prop is available on all components that render a day calendar, including t The example below adds a dot at the end of each day in the calendar header: {{"demo": "CustomDayOfWeekFormat.js"}} - -## Use UTC dates - -### With dayjs - -To use UTC dates with `dayjs`, you have to: - -1. Extend `dayjs` with its `utc` plugin: - - ```tsx - import dayjs from 'dayjs'; - import utc from 'dayjs/plugin/utc'; - - dayjs.extend(utc); - ``` - -2. Pass `dayjs.utc` to `LocalizationProvider` `dateLibInstance` prop: - - ```tsx - - {children} - - ``` - -3. Always pass dates created with `dayjs.utc`: - - ```tsx - - ``` - -{{"demo": "UTCDayjs.js", "defaultCodeOpen": false}} - -### With moment - -To use UTC dates with `moment`, you have to: - -1. Pass `moment.utc` to `LocalizationProvider` `dateLibInstance` prop: - - ```tsx - - {children} - - ``` - -2. Always pass dates created with `moment.utc`: - - ```tsx - - ``` - -{{"demo": "UTCMoment.js", "defaultCodeOpen": false}} - -### Other libraries - -UTC support is an ongoing topic. - -We will update the documentation with examples using other date libraries once the support for them will be sufficient. diff --git a/docs/data/date-pickers/timezone/BasicTimezoneProp.js b/docs/data/date-pickers/timezone/BasicTimezoneProp.js new file mode 100644 index 0000000000000..ee60552caa01b --- /dev/null +++ b/docs/data/date-pickers/timezone/BasicTimezoneProp.js @@ -0,0 +1,38 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { TimePicker } from '@mui/x-date-pickers/TimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function BasicTimezoneProp() { + const [value, setValue] = React.useState(dayjs.utc('2022-04-17T15:30')); + + return ( + + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/BasicTimezoneProp.tsx b/docs/data/date-pickers/timezone/BasicTimezoneProp.tsx new file mode 100644 index 0000000000000..8f0d275fbcc39 --- /dev/null +++ b/docs/data/date-pickers/timezone/BasicTimezoneProp.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { TimePicker } from '@mui/x-date-pickers/TimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function BasicTimezoneProp() { + const [value, setValue] = React.useState( + dayjs.utc('2022-04-17T15:30'), + ); + + return ( + + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/BasicTimezoneProp.tsx.preview b/docs/data/date-pickers/timezone/BasicTimezoneProp.tsx.preview new file mode 100644 index 0000000000000..cbaa66bd22ab1 --- /dev/null +++ b/docs/data/date-pickers/timezone/BasicTimezoneProp.tsx.preview @@ -0,0 +1,15 @@ + + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/BasicValueProp.js b/docs/data/date-pickers/timezone/BasicValueProp.js new file mode 100644 index 0000000000000..bda00ae691c47 --- /dev/null +++ b/docs/data/date-pickers/timezone/BasicValueProp.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { TimePicker } from '@mui/x-date-pickers/TimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function BasicValueProp() { + const [value, setValue] = React.useState( + dayjs.tz('2022-04-17T15:30', 'America/New_York'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/BasicValueProp.tsx b/docs/data/date-pickers/timezone/BasicValueProp.tsx new file mode 100644 index 0000000000000..badbc27f17ec2 --- /dev/null +++ b/docs/data/date-pickers/timezone/BasicValueProp.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { TimePicker } from '@mui/x-date-pickers/TimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function BasicValueProp() { + const [value, setValue] = React.useState( + dayjs.tz('2022-04-17T15:30', 'America/New_York'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/BasicValueProp.tsx.preview b/docs/data/date-pickers/timezone/BasicValueProp.tsx.preview new file mode 100644 index 0000000000000..c44c0adc58d0c --- /dev/null +++ b/docs/data/date-pickers/timezone/BasicValueProp.tsx.preview @@ -0,0 +1,8 @@ + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/DayjsTimezone.js b/docs/data/date-pickers/timezone/DayjsTimezone.js new file mode 100644 index 0000000000000..60c5f23dfb1e4 --- /dev/null +++ b/docs/data/date-pickers/timezone/DayjsTimezone.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function DayjsTimezone() { + const [value, setValue] = React.useState( + dayjs.tz('2022-04-17T15:30', 'America/New_York'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/DayjsTimezone.tsx b/docs/data/date-pickers/timezone/DayjsTimezone.tsx new file mode 100644 index 0000000000000..c23e027f112d0 --- /dev/null +++ b/docs/data/date-pickers/timezone/DayjsTimezone.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function DayjsTimezone() { + const [value, setValue] = React.useState( + dayjs.tz('2022-04-17T15:30', 'America/New_York'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/DayjsTimezone.tsx.preview b/docs/data/date-pickers/timezone/DayjsTimezone.tsx.preview new file mode 100644 index 0000000000000..1ea656e495480 --- /dev/null +++ b/docs/data/date-pickers/timezone/DayjsTimezone.tsx.preview @@ -0,0 +1,4 @@ + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/DayjsUTC.js b/docs/data/date-pickers/timezone/DayjsUTC.js new file mode 100644 index 0000000000000..52ae28413111f --- /dev/null +++ b/docs/data/date-pickers/timezone/DayjsUTC.js @@ -0,0 +1,25 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); + +export default function DayjsUTC() { + const [value, setValue] = React.useState(dayjs.utc('2022-04-17T15:30')); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/DayjsUTC.tsx b/docs/data/date-pickers/timezone/DayjsUTC.tsx new file mode 100644 index 0000000000000..6751e5106f569 --- /dev/null +++ b/docs/data/date-pickers/timezone/DayjsUTC.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); + +export default function DayjsUTC() { + const [value, setValue] = React.useState( + dayjs.utc('2022-04-17T15:30'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/DayjsUTC.tsx.preview b/docs/data/date-pickers/timezone/DayjsUTC.tsx.preview new file mode 100644 index 0000000000000..1ea656e495480 --- /dev/null +++ b/docs/data/date-pickers/timezone/DayjsUTC.tsx.preview @@ -0,0 +1,4 @@ + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/LuxonTimezone.js b/docs/data/date-pickers/timezone/LuxonTimezone.js new file mode 100644 index 0000000000000..b95acbe374fd5 --- /dev/null +++ b/docs/data/date-pickers/timezone/LuxonTimezone.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { DateTime } from 'luxon'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function LuxonTimezone() { + const [value, setValue] = React.useState( + DateTime.fromISO('2022-04-17T15:30', { zone: 'America/New_York' }), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.toISO()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/LuxonTimezone.tsx b/docs/data/date-pickers/timezone/LuxonTimezone.tsx new file mode 100644 index 0000000000000..a80ccfbc26f8c --- /dev/null +++ b/docs/data/date-pickers/timezone/LuxonTimezone.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { DateTime } from 'luxon'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function LuxonTimezone() { + const [value, setValue] = React.useState( + DateTime.fromISO('2022-04-17T15:30', { zone: 'America/New_York' }), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.toISO()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/LuxonTimezone.tsx.preview b/docs/data/date-pickers/timezone/LuxonTimezone.tsx.preview new file mode 100644 index 0000000000000..832e4098d0610 --- /dev/null +++ b/docs/data/date-pickers/timezone/LuxonTimezone.tsx.preview @@ -0,0 +1,4 @@ + + + Stored value: {value == null ? 'null' : value.toISO()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/LuxonUTC.js b/docs/data/date-pickers/timezone/LuxonUTC.js new file mode 100644 index 0000000000000..083169c6791b7 --- /dev/null +++ b/docs/data/date-pickers/timezone/LuxonUTC.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { DateTime } from 'luxon'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function LuxonUTC() { + const [value, setValue] = React.useState( + DateTime.fromISO('2022-04-17T15:30', { zone: 'UTC' }), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.toISO()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/LuxonUTC.tsx b/docs/data/date-pickers/timezone/LuxonUTC.tsx new file mode 100644 index 0000000000000..fad298a38155f --- /dev/null +++ b/docs/data/date-pickers/timezone/LuxonUTC.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { DateTime } from 'luxon'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function LuxonUTC() { + const [value, setValue] = React.useState( + DateTime.fromISO('2022-04-17T15:30', { zone: 'UTC' }), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.toISO()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/LuxonUTC.tsx.preview b/docs/data/date-pickers/timezone/LuxonUTC.tsx.preview new file mode 100644 index 0000000000000..832e4098d0610 --- /dev/null +++ b/docs/data/date-pickers/timezone/LuxonUTC.tsx.preview @@ -0,0 +1,4 @@ + + + Stored value: {value == null ? 'null' : value.toISO()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/MomentTimezone.js b/docs/data/date-pickers/timezone/MomentTimezone.js new file mode 100644 index 0000000000000..7f83d4149195a --- /dev/null +++ b/docs/data/date-pickers/timezone/MomentTimezone.js @@ -0,0 +1,24 @@ +import * as React from 'react'; +import moment from 'moment-timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function MomentTimezone() { + const [value, setValue] = React.useState( + moment.tz('2022-04-17T15:30', 'America/New_York'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/MomentTimezone.tsx b/docs/data/date-pickers/timezone/MomentTimezone.tsx new file mode 100644 index 0000000000000..5a56658726ff5 --- /dev/null +++ b/docs/data/date-pickers/timezone/MomentTimezone.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import moment, { Moment } from 'moment-timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function MomentTimezone() { + const [value, setValue] = React.useState( + moment.tz('2022-04-17T15:30', 'America/New_York'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/MomentTimezone.tsx.preview b/docs/data/date-pickers/timezone/MomentTimezone.tsx.preview new file mode 100644 index 0000000000000..1ea656e495480 --- /dev/null +++ b/docs/data/date-pickers/timezone/MomentTimezone.tsx.preview @@ -0,0 +1,4 @@ + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/MomentUTC.js b/docs/data/date-pickers/timezone/MomentUTC.js new file mode 100644 index 0000000000000..ba64bfeec668c --- /dev/null +++ b/docs/data/date-pickers/timezone/MomentUTC.js @@ -0,0 +1,22 @@ +import * as React from 'react'; +import moment from 'moment'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function MomentUTC() { + const [value, setValue] = React.useState(moment.utc('2022-04-17T15:30')); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/MomentUTC.tsx b/docs/data/date-pickers/timezone/MomentUTC.tsx new file mode 100644 index 0000000000000..50fb534b844ad --- /dev/null +++ b/docs/data/date-pickers/timezone/MomentUTC.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import moment, { Moment } from 'moment'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +export default function MomentUTC() { + const [value, setValue] = React.useState( + moment.utc('2022-04-17T15:30'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/MomentUTC.tsx.preview b/docs/data/date-pickers/timezone/MomentUTC.tsx.preview new file mode 100644 index 0000000000000..1ea656e495480 --- /dev/null +++ b/docs/data/date-pickers/timezone/MomentUTC.tsx.preview @@ -0,0 +1,4 @@ + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.js b/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.js new file mode 100644 index 0000000000000..4e1cfd59a10d2 --- /dev/null +++ b/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.js @@ -0,0 +1,31 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function StoreUTCButDisplayOtherTimezone() { + const [value, setValue] = React.useState(dayjs.utc('2022-04-17T15:30')); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx b/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx new file mode 100644 index 0000000000000..a8eb29d16adea --- /dev/null +++ b/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function StoreUTCButDisplayOtherTimezone() { + const [value, setValue] = React.useState( + dayjs.utc('2022-04-17T15:30'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx.preview b/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx.preview new file mode 100644 index 0000000000000..54d958df245fc --- /dev/null +++ b/docs/data/date-pickers/timezone/StoreUTCButDisplayOtherTimezone.tsx.preview @@ -0,0 +1,8 @@ + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.js b/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.js new file mode 100644 index 0000000000000..41e1fb185613e --- /dev/null +++ b/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.js @@ -0,0 +1,27 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function StoreUTCButDisplaySystemTimezone() { + const [value, setValue] = React.useState(dayjs.utc('2022-04-17T15:30')); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx b/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx new file mode 100644 index 0000000000000..38dbabce90b9c --- /dev/null +++ b/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +export default function StoreUTCButDisplaySystemTimezone() { + const [value, setValue] = React.useState( + dayjs.utc('2022-04-17T15:30'), + ); + + return ( + + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx.preview b/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx.preview new file mode 100644 index 0000000000000..1deb7b52e4140 --- /dev/null +++ b/docs/data/date-pickers/timezone/StoreUTCButDisplaySystemTimezone.tsx.preview @@ -0,0 +1,4 @@ + + + Stored value: {value == null ? 'null' : value.format()} + \ No newline at end of file diff --git a/docs/data/date-pickers/timezone/TimezonePlayground.js b/docs/data/date-pickers/timezone/TimezonePlayground.js new file mode 100644 index 0000000000000..6db15ee8f7a65 --- /dev/null +++ b/docs/data/date-pickers/timezone/TimezonePlayground.js @@ -0,0 +1,55 @@ +import * as React from 'react'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import ToggleButton from '@mui/material/ToggleButton'; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.tz.setDefault('America/New_York'); + +const TIMEZONES = ['default', 'system', 'UTC', 'America/Chicago']; + +export default function TimezonePlayground() { + const [value, setValue] = React.useState(dayjs.utc('2022-04-17T15:30')); + + const [currentTimezone, setCurrentTimezone] = React.useState('UTC'); + + return ( + + + { + if (newTimezone != null) { + setCurrentTimezone(newTimezone); + } + }} + > + {TIMEZONES.map((timezoneName) => ( + + {timezoneName} + + ))} + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/TimezonePlayground.tsx b/docs/data/date-pickers/timezone/TimezonePlayground.tsx new file mode 100644 index 0000000000000..d6953cb467d59 --- /dev/null +++ b/docs/data/date-pickers/timezone/TimezonePlayground.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { PickersTimezone } from '@mui/x-date-pickers/models'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import ToggleButton from '@mui/material/ToggleButton'; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.tz.setDefault('America/New_York'); + +const TIMEZONES: PickersTimezone[] = ['default', 'system', 'UTC', 'America/Chicago']; + +export default function TimezonePlayground() { + const [value, setValue] = React.useState( + dayjs.utc('2022-04-17T15:30'), + ); + + const [currentTimezone, setCurrentTimezone] = React.useState('UTC'); + + return ( + + + { + if (newTimezone != null) { + setCurrentTimezone(newTimezone); + } + }} + > + {TIMEZONES.map((timezoneName) => ( + + {timezoneName} + + ))} + + + + Stored value: {value == null ? 'null' : value.format()} + + + + ); +} diff --git a/docs/data/date-pickers/timezone/timezone.md b/docs/data/date-pickers/timezone/timezone.md new file mode 100644 index 0000000000000..e686b8286df7b --- /dev/null +++ b/docs/data/date-pickers/timezone/timezone.md @@ -0,0 +1,383 @@ +--- +product: date-pickers +title: Date and Time pickers - UTC and timezones +components: LocalizationProvider +githubLabel: 'component: pickers' +packageName: '@mui/x-date-pickers' +--- + +# UTC and timezones + +

Date and Time Pickers support UTC and timezones.

+ +:::warning +UTC and timezone support is an ongoing topic. + +Only `AdapterDayjs`, `AdapterLuxon` and `AdapterMoment` are currently compatible with UTC dates and timezones. +::: + +## Overview + +By default, the components will always use the timezone of your `value` / `defaultValue` prop: + +{{"demo": "BasicValueProp.js", "defaultCodeOpen": false}} + +You can use the `timezone` prop to explicitly define the timezone in which the value should be rendered: + +{{"demo": "BasicTimezoneProp.js"}} + +This will be needed if the component has no `value` or `defaultValue` to deduct the timezone from it or if you don't want to render the value in its original timezone. + +## Supported timezones + +| Timezone | Description | +| ------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `"UTC"` | Will use the [Coordinated Universal Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) | +| `"default"` | Will use the default timezone of your date library, this value can be set using
- [`dayjs.tz.setDefault`](https://day.js.org/docs/en/timezone/set-default-timezone) on dayjs
- [`Settings.defaultZone`](https://moment.github.io/luxon/#/zones?id=changing-the-default-zone) on luxon
- [`moment.tz.setDefault`](https://momentjs.com/timezone/docs/#/using-timezones/default-timezone/) on moment | +| `"system"` | Will use the system's local timezone | +| IANA standard zones | Example: `"Europe/Paris"`, `"America/New_York"`
[List of all the IANA zones](https://timezonedb.com/time-zones) | +| Fixed offset | Example: `"UTC+7"`
**Only available with Luxon** | + +{{"demo": "TimezonePlayground.js", "defaultCodeOpen": false}} + +## Usage with Day.js + +### Day.js and UTC + +Before using the UTC dates with Day.js, you have to enable the `utc` plugin: + +```tsx +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +dayjs.extend(utc); +``` + +:::info +**How to create a UTC date with Day.js?** + +To create a UTC date, use the `dayjs.utc` method + +```tsx +const date = dayjs.utc('2022-04-17T15:30'); +``` + +You can check out the documentation of the [UTC on Day.js](https://day.js.org/docs/en/plugin/utc) for more details. +::: + +You can then pass your UTC date to your picker: + +```tsx +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); + +function App() { + return ( + + + + ); +} +``` + +{{"demo": "DayjsUTC.js", "defaultCodeOpen": false}} + +### Day.js and timezones + +Before using the timezone with Day.js, you have to enable both the `utc` and `timezone` plugins: + +```tsx +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; + +dayjs.extend(utc); +dayjs.extend(timezone); +``` + +:::info +**How to create a date in a specific timezone with Day.js?** + +If your whole application is using dates from the same timezone, set the default zone to your timezone name: + +```tsx +import { dayjs } from 'dayjs'; + +dayjs.tz.setDefault('America/New_York'); + +const date = dayjs.tz('2022-04-17T15:30'); +``` + +If you only want to use dates with this timezone on some parts of your application, pass the timezone as the 2nd parameter of the `dayjs.tz` method: + +```tsx +import { dayjs } from 'dayjs'; + +const date = dayjs.tz('2022-04-17T15:30', 'America/New_York'); +``` + +You can check out the documentation of the [timezone on Day.js](https://day.js.org/docs/en/timezone/timezone) for more details. +::: + +You can then pass your date in the wanted timezone to your picker: + +```tsx +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; + +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +function App() { + return ( + + + + ); +} +``` + +{{"demo": "DayjsTimezone.js", "defaultCodeOpen": false}} + +:::info +Please check out the documentation of the [dayjs timezone plugin](https://day.js.org/docs/en/timezone/timezone) for more details on how to manipulate the timezones. +::: + +## Usage with Luxon + +### Luxon and UTC + +:::info +**How to create a UTC date with Luxon?** + +If your whole application is using UTC dates, set the default zone to `"UTC"`: + +```tsx +import { DateTime, Settings } from 'luxon'; + +Settings.defaultZone = 'UTC'; + +const date1 = DateTime.fromISO('2022-04-17T15:30'); +const date2 = DateTime.fromSQL('2022-04-17 15:30:00'); +``` + +If you only want to use UTC dates on some parts of your application, create a UTC date using `DateTime.utc` or with the `zone` parameter of Luxon methods: + +```tsx +import { DateTime } from 'luxon'; + +const date1 = DateTime.utc(2022, 4, 17, 15, 30); +const date2 = DateTime.fromISO('2022-04-17T15:30', { zone: 'UTC' }); +const date3 = DateTime.fromSQL('2022-04-17 15:30:00', { zone: 'UTC' }); +``` + +Please check out the documentation of the [UTC and timezone on Luxon](https://moment.github.io/luxon/#/zones) for more details. +::: + +You can then pass your UTC date to your picker: + +```tsx +import { DateTime, Settings } from 'luxon'; + +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +function App() { + return ( + + + + ); +} +``` + +{{"demo": "LuxonUTC.js", "defaultCodeOpen": false}} + +### Luxon and timezone + +:::info +**How to create a date in a specific timezone with Luxon?** + +If your whole application is using dates from the same timezone, set the default zone to your timezone name: + +```tsx +import { DateTime, Settings } from 'luxon'; + +Settings.defaultZone = 'America/New_York'; + +const date1 = DateTime.fromISO('2022-04-17T15:30'); +const date2 = DateTime.fromSQL('2022-04-17 15:30:00'); +``` + +If you only want to use dates with this timezone on some parts of your application, create a date in this timezone using the `zone` parameter of Luxon methods: + +```tsx +import { DateTime } from 'luxon'; + +const date1 = DateTime.fromISO('2022-04-17T15:30', { zone: 'America/New_York' }); +const date2 = DateTime.fromSQL('2022-04-17 15:30:00', { zone: 'America/New_York' }); +``` + +Please check out the documentation of the [UTC and timezone on Luxon](https://moment.github.io/luxon/#/zones) for more details. +::: + +You can then pass your date in the wanted timezone to your picker: + +```tsx +import { DateTime, Settings } from 'luxon'; + +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +function App() { + return ( + + + + ); +} +``` + +{{"demo": "LuxonTimezone.js", "defaultCodeOpen": false}} + +:::info +Please check out the documentation of the [UTC and timezone on Luxon](https://moment.github.io/luxon/#/zones) for more details on how to manipulate the timezones. +::: + +## Usage with Moment + +### Moment and UTC + +:::info +**How to create a UTC date with Moment?** + +To create a UTC date, use the `dayjs.utc` method + +```tsx +const date = moment.utc('2022-04-17T15:30'); +``` + +Please check out the documentation of the [UTC on Moment](https://momentjs.com/docs/#/parsing/utc/) for more details. +::: + +You can then pass your UTC date to your picker: + +```tsx +import moment from 'moment'; + +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +function App() { + return ( + + + + ); +} +``` + +{{"demo": "MomentUTC.js", "defaultCodeOpen": false}} + +### Moment and timezone + +Before using the timezone with Day.js, you have to pass the default export from `moment-timezone` to the `dateLibInstance` prop of `LocalizationProvider`: + +```tsx +import moment from 'moment-timezone'; + +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; + + + {children} +; +``` + +:::info +**How to create a date in a specific timezone with Moment?** + +If your whole application is using dates from the same timezone, set the default zone to your timezone name: + +```tsx +import moment from 'moment-timezone'; + +moment.tz.setDefault('America/New_York'); + +const date = moment('2022-04-17T15:30'); +``` + +If you only want to use dates with this timezone on some parts of your application, create a date using the `moment.tz` method: + +```tsx +import moment from 'moment-timezone'; + +const date = moment.tz('2022-04-17T15:30', 'America/New_York'); +``` + +Please check out the documentation of the [timezone on Moment](https://momentjs.com/timezone/) for more details. +::: + +You can then pass your date in the wanted timezone to your picker: + +```tsx +import moment from 'moment-timezone'; + +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +function App() { + return ( + + + + ); +} +``` + +{{"demo": "MomentTimezone.js", "defaultCodeOpen": false}} + +## More advanced examples + +:::info +The following examples are all built using `dayjs`. +You can achieve the exact same behavior using `luxon` or `moment`, +please refer to the sections above to know how to pass a UTC date or a date in a specific timezone to your component. +::: + +### Store UTC dates but display in system's timezone + +The demo below shows how to store dates in UTC while displaying using the system timezone. + +{{"demo": "StoreUTCButDisplaySystemTimezone.js", "defaultCodeOpen": false}} + +### Store UTC dates but display in another timezone + +The demo below shows how to store dates in UTC while displaying using the `Pacific/Honolulu` timezone. + +{{"demo": "StoreUTCButDisplayOtherTimezone.js", "defaultCodeOpen": false}} diff --git a/docs/data/pages.ts b/docs/data/pages.ts index 25355b7f1c5b7..a8d7423029c7c 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -287,6 +287,10 @@ const pages: MuiPage[] = [ pathname: '/x/react-date-pickers/adapters-locale', title: 'Date localization', }, + { + pathname: '/x/react-date-pickers/timezone', + title: 'UTC and timezone', + }, { pathname: '/x/react-date-pickers/localization', title: 'Component localization', diff --git a/docs/package.json b/docs/package.json index 2b0b024af5d49..43747a08c4a90 100644 --- a/docs/package.json +++ b/docs/package.json @@ -65,6 +65,7 @@ "marked": "^4.3.0", "moment": "^2.29.4", "moment-hijri": "^2.1.2", + "moment-timezone": "^0.5.41", "next": "^13.3.1", "nprogress": "^0.2.0", "postcss": "^8.4.24", diff --git a/docs/pages/x/api/date-pickers/date-calendar.json b/docs/pages/x/api/date-pickers/date-calendar.json index a72be5475a948..e1efe47d8cdd9 100644 --- a/docs/pages/x/api/date-pickers/date-calendar.json +++ b/docs/pages/x/api/date-pickers/date-calendar.json @@ -74,6 +74,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/date-field.json b/docs/pages/x/api/date-pickers/date-field.json index 21947fd0c88bf..f939afd0dcbb1 100644 --- a/docs/pages/x/api/date-pickers/date-field.json +++ b/docs/pages/x/api/date-pickers/date-field.json @@ -80,6 +80,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "unstableFieldRef": { "type": { "name": "union", "description": "func
| object" } }, diff --git a/docs/pages/x/api/date-pickers/date-picker.json b/docs/pages/x/api/date-pickers/date-picker.json index a8bbc9a16a131..5966b4ff94690 100644 --- a/docs/pages/x/api/date-pickers/date-picker.json +++ b/docs/pages/x/api/date-pickers/date-picker.json @@ -97,6 +97,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/date-range-calendar.json b/docs/pages/x/api/date-pickers/date-range-calendar.json index fea4cc72d84fd..4fff749fd1760 100644 --- a/docs/pages/x/api/date-pickers/date-range-calendar.json +++ b/docs/pages/x/api/date-pickers/date-range-calendar.json @@ -62,6 +62,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } } }, "slots": { diff --git a/docs/pages/x/api/date-pickers/date-range-picker.json b/docs/pages/x/api/date-pickers/date-range-picker.json index e928fb90d63f0..081cedf2b3536 100644 --- a/docs/pages/x/api/date-pickers/date-range-picker.json +++ b/docs/pages/x/api/date-pickers/date-range-picker.json @@ -93,6 +93,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } }, "viewRenderers": { "type": { "name": "shape", "description": "{ day?: func }" } } }, diff --git a/docs/pages/x/api/date-pickers/date-time-field.json b/docs/pages/x/api/date-pickers/date-time-field.json index 7c78ed5b5139a..d3ffe05ae2f0a 100644 --- a/docs/pages/x/api/date-pickers/date-time-field.json +++ b/docs/pages/x/api/date-pickers/date-time-field.json @@ -93,6 +93,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "unstableFieldRef": { "type": { "name": "union", "description": "func
| object" } }, 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 f49d0cbce7648..3a36cff82f339 100644 --- a/docs/pages/x/api/date-pickers/date-time-picker.json +++ b/docs/pages/x/api/date-pickers/date-time-picker.json @@ -119,6 +119,10 @@ }, "default": "{ hours: 1, minutes: 5, seconds: 5 }" }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/desktop-date-picker.json b/docs/pages/x/api/date-pickers/desktop-date-picker.json index 544f92357b04f..40c3325cb14fa 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-picker.json @@ -93,6 +93,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/desktop-date-range-picker.json b/docs/pages/x/api/date-pickers/desktop-date-range-picker.json index f4b8670f3d0f7..6de3b0e9ca072 100644 --- a/docs/pages/x/api/date-pickers/desktop-date-range-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-date-range-picker.json @@ -89,6 +89,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } }, "viewRenderers": { "type": { "name": "shape", "description": "{ day?: func }" } } }, 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 f3fdf82449092..7552027732839 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 @@ -115,6 +115,10 @@ }, "default": "{ hours: 1, minutes: 5, seconds: 5 }" }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/desktop-time-picker.json b/docs/pages/x/api/date-pickers/desktop-time-picker.json index 0abe508392378..506a924aa6a2d 100644 --- a/docs/pages/x/api/date-pickers/desktop-time-picker.json +++ b/docs/pages/x/api/date-pickers/desktop-time-picker.json @@ -85,6 +85,10 @@ }, "default": "{ hours: 1, minutes: 5, seconds: 5 }" }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/digital-clock.json b/docs/pages/x/api/date-pickers/digital-clock.json index c2c025c74cba4..e7840930fb61d 100644 --- a/docs/pages/x/api/date-pickers/digital-clock.json +++ b/docs/pages/x/api/date-pickers/digital-clock.json @@ -45,6 +45,10 @@ } }, "timeStep": { "type": { "name": "number" }, "default": "30" }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { "name": "enum", "description": "'hours'" } }, "views": { "type": { "name": "arrayOf", "description": "Array<'hours'>" } } diff --git a/docs/pages/x/api/date-pickers/localization-provider.json b/docs/pages/x/api/date-pickers/localization-provider.json index 41394bc5200d8..688a338add846 100644 --- a/docs/pages/x/api/date-pickers/localization-provider.json +++ b/docs/pages/x/api/date-pickers/localization-provider.json @@ -16,7 +16,7 @@ "styles": { "classes": [], "globalClasses": {}, "name": "MuiLocalizationProvider" }, "filename": "/packages/x-date-pickers/src/LocalizationProvider/LocalizationProvider.tsx", "inheritance": null, - "demos": "", + "demos": "", "packages": [ { "packageName": "@mui/x-date-pickers-pro", "componentName": "LocalizationProvider" }, { "packageName": "@mui/x-date-pickers", "componentName": "LocalizationProvider" } diff --git a/docs/pages/x/api/date-pickers/mobile-date-picker.json b/docs/pages/x/api/date-pickers/mobile-date-picker.json index 0030beb6c2228..fb83eb2696d3c 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-picker.json @@ -93,6 +93,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/mobile-date-range-picker.json b/docs/pages/x/api/date-pickers/mobile-date-range-picker.json index ab89b280d3a6d..86e78395d7879 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-range-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-range-picker.json @@ -89,6 +89,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } }, "viewRenderers": { "type": { "name": "shape", "description": "{ day?: func }" } } }, diff --git a/docs/pages/x/api/date-pickers/mobile-date-time-picker.json b/docs/pages/x/api/date-pickers/mobile-date-time-picker.json index 4b3f16dc3367c..1b187c52177f3 100644 --- a/docs/pages/x/api/date-pickers/mobile-date-time-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-date-time-picker.json @@ -107,6 +107,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/mobile-time-picker.json b/docs/pages/x/api/date-pickers/mobile-time-picker.json index f4d3d51efc3c4..a4a697dfc676e 100644 --- a/docs/pages/x/api/date-pickers/mobile-time-picker.json +++ b/docs/pages/x/api/date-pickers/mobile-time-picker.json @@ -76,6 +76,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/month-calendar.json b/docs/pages/x/api/date-pickers/month-calendar.json index 1200ee9869301..2450e99c4d573 100644 --- a/docs/pages/x/api/date-pickers/month-calendar.json +++ b/docs/pages/x/api/date-pickers/month-calendar.json @@ -26,6 +26,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } } }, "slots": {}, diff --git a/docs/pages/x/api/date-pickers/multi-input-date-range-field.json b/docs/pages/x/api/date-pickers/multi-input-date-range-field.json index f986def90f7cc..4fd215351a3a0 100644 --- a/docs/pages/x/api/date-pickers/multi-input-date-range-field.json +++ b/docs/pages/x/api/date-pickers/multi-input-date-range-field.json @@ -62,6 +62,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "useFlexGap": { "type": { "name": "bool" } }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } } }, diff --git a/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json b/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json index 5774199b7f4b2..bc566422505eb 100644 --- a/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json +++ b/docs/pages/x/api/date-pickers/multi-input-date-time-range-field.json @@ -75,6 +75,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "useFlexGap": { "type": { "name": "bool" } }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } } }, diff --git a/docs/pages/x/api/date-pickers/multi-input-time-range-field.json b/docs/pages/x/api/date-pickers/multi-input-time-range-field.json index 6b19c279e303d..c7d85cbef5581 100644 --- a/docs/pages/x/api/date-pickers/multi-input-time-range-field.json +++ b/docs/pages/x/api/date-pickers/multi-input-time-range-field.json @@ -70,6 +70,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "useFlexGap": { "type": { "name": "bool" } }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } } }, 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 f335cfc4437ec..9c89cc98e9243 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 @@ -61,6 +61,10 @@ }, "default": "{ hours: 1, minutes: 5, seconds: 5 }" }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/single-input-date-range-field.json b/docs/pages/x/api/date-pickers/single-input-date-range-field.json index 46aecde83432b..69a46d831631c 100644 --- a/docs/pages/x/api/date-pickers/single-input-date-range-field.json +++ b/docs/pages/x/api/date-pickers/single-input-date-range-field.json @@ -78,6 +78,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "unstableFieldRef": { "type": { "name": "union", "description": "func
| object" } }, diff --git a/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json b/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json index 3abbdcf088998..895ca20162126 100644 --- a/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json +++ b/docs/pages/x/api/date-pickers/single-input-date-time-range-field.json @@ -91,6 +91,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "unstableFieldRef": { "type": { "name": "union", "description": "func
| object" } }, diff --git a/docs/pages/x/api/date-pickers/single-input-time-range-field.json b/docs/pages/x/api/date-pickers/single-input-time-range-field.json index f0a82603f7da0..6c966d23305d1 100644 --- a/docs/pages/x/api/date-pickers/single-input-time-range-field.json +++ b/docs/pages/x/api/date-pickers/single-input-time-range-field.json @@ -86,6 +86,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "unstableFieldRef": { "type": { "name": "union", "description": "func
| object" } }, diff --git a/docs/pages/x/api/date-pickers/static-date-picker.json b/docs/pages/x/api/date-pickers/static-date-picker.json index 43e7dc9306901..055651b592e3e 100644 --- a/docs/pages/x/api/date-pickers/static-date-picker.json +++ b/docs/pages/x/api/date-pickers/static-date-picker.json @@ -78,6 +78,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/static-date-range-picker.json b/docs/pages/x/api/date-pickers/static-date-range-picker.json index dea3454382fe7..87a8e388b611e 100644 --- a/docs/pages/x/api/date-pickers/static-date-range-picker.json +++ b/docs/pages/x/api/date-pickers/static-date-range-picker.json @@ -74,6 +74,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "arrayOf", "description": "Array<any>" } }, "viewRenderers": { "type": { "name": "shape", "description": "{ day?: func }" } } }, diff --git a/docs/pages/x/api/date-pickers/static-date-time-picker.json b/docs/pages/x/api/date-pickers/static-date-time-picker.json index eaf093140dbee..3e6765e44e2b7 100644 --- a/docs/pages/x/api/date-pickers/static-date-time-picker.json +++ b/docs/pages/x/api/date-pickers/static-date-time-picker.json @@ -92,6 +92,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/static-time-picker.json b/docs/pages/x/api/date-pickers/static-time-picker.json index a3be4a94a47cf..5696f30420cfe 100644 --- a/docs/pages/x/api/date-pickers/static-time-picker.json +++ b/docs/pages/x/api/date-pickers/static-time-picker.json @@ -61,6 +61,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/time-clock.json b/docs/pages/x/api/date-pickers/time-clock.json index 2fd89adb17ca3..1c2f09a3a8ed6 100644 --- a/docs/pages/x/api/date-pickers/time-clock.json +++ b/docs/pages/x/api/date-pickers/time-clock.json @@ -54,6 +54,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/time-field.json b/docs/pages/x/api/date-pickers/time-field.json index 6098ad6eaa951..cc4799ca1f521 100644 --- a/docs/pages/x/api/date-pickers/time-field.json +++ b/docs/pages/x/api/date-pickers/time-field.json @@ -86,6 +86,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "unstableFieldRef": { "type": { "name": "union", "description": "func
| object" } }, diff --git a/docs/pages/x/api/date-pickers/time-picker.json b/docs/pages/x/api/date-pickers/time-picker.json index 1cd21a97e9fff..b7c11ca5104e2 100644 --- a/docs/pages/x/api/date-pickers/time-picker.json +++ b/docs/pages/x/api/date-pickers/time-picker.json @@ -89,6 +89,10 @@ }, "default": "{ hours: 1, minutes: 5, seconds: 5 }" }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "view": { "type": { diff --git a/docs/pages/x/api/date-pickers/year-calendar.json b/docs/pages/x/api/date-pickers/year-calendar.json index e8c548f7cb517..37b1fdb3026e1 100644 --- a/docs/pages/x/api/date-pickers/year-calendar.json +++ b/docs/pages/x/api/date-pickers/year-calendar.json @@ -22,6 +22,10 @@ "description": "Array<func
| object
| bool>
| func
| object" } }, + "timezone": { + "type": { "name": "string" }, + "default": "The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise." + }, "value": { "type": { "name": "any" } }, "yearsPerRow": { "type": { "name": "enum", "description": "3
| 4" }, diff --git a/docs/pages/x/react-date-pickers/timezone.js b/docs/pages/x/react-date-pickers/timezone.js new file mode 100644 index 0000000000000..23ff5978628f9 --- /dev/null +++ b/docs/pages/x/react-date-pickers/timezone.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/date-pickers/timezone/timezone.md?@mui/markdown'; + +export default function Page() { + return ; +} diff --git a/docs/translations/api-docs/date-pickers/date-calendar.json b/docs/translations/api-docs/date-pickers/date-calendar.json index 83276f81275af..c0581a0b93826 100644 --- a/docs/translations/api-docs/date-pickers/date-calendar.json +++ b/docs/translations/api-docs/date-pickers/date-calendar.json @@ -35,6 +35,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", "views": "Available views.", diff --git a/docs/translations/api-docs/date-pickers/date-field.json b/docs/translations/api-docs/date-pickers/date-field.json index 636b722b561c1..898f7b9cc2fb2 100644 --- a/docs/translations/api-docs/date-pickers/date-field.json +++ b/docs/translations/api-docs/date-pickers/date-field.json @@ -41,6 +41,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "unstableFieldRef": "The ref object used to imperatively interact with the field.", "value": "The selected value. Used when the component is controlled.", "variant": "The variant to use." diff --git a/docs/translations/api-docs/date-pickers/date-picker.json b/docs/translations/api-docs/date-pickers/date-picker.json index 56109ba55eac0..67ef207657e87 100644 --- a/docs/translations/api-docs/date-pickers/date-picker.json +++ b/docs/translations/api-docs/date-pickers/date-picker.json @@ -48,6 +48,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/date-range-calendar.json b/docs/translations/api-docs/date-pickers/date-range-calendar.json index 33e72ee7f7506..e0e8233d58cfd 100644 --- a/docs/translations/api-docs/date-pickers/date-range-calendar.json +++ b/docs/translations/api-docs/date-pickers/date-range-calendar.json @@ -33,6 +33,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "value": "The selected value. Used when the component is controlled." }, "classDescriptions": { diff --git a/docs/translations/api-docs/date-pickers/date-range-picker.json b/docs/translations/api-docs/date-pickers/date-range-picker.json index 4eae2029ac733..28495c6ca00a6 100644 --- a/docs/translations/api-docs/date-pickers/date-range-picker.json +++ b/docs/translations/api-docs/date-pickers/date-range-picker.json @@ -48,6 +48,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "value": "The selected value. Used when the component is controlled.", "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." }, diff --git a/docs/translations/api-docs/date-pickers/date-time-field.json b/docs/translations/api-docs/date-pickers/date-time-field.json index 24c8569888126..98c19d2095122 100644 --- a/docs/translations/api-docs/date-pickers/date-time-field.json +++ b/docs/translations/api-docs/date-pickers/date-time-field.json @@ -50,6 +50,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "unstableFieldRef": "The ref object used to imperatively interact with the field.", "value": "The selected value. Used when the component is controlled.", "variant": "The variant to use." 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 030e67379dfb7..a6f6f96639d68 100644 --- a/docs/translations/api-docs/date-pickers/date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/date-time-picker.json @@ -60,6 +60,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/desktop-date-picker.json b/docs/translations/api-docs/date-pickers/desktop-date-picker.json index 6805ae374b6e3..24e6b1e863e0d 100644 --- a/docs/translations/api-docs/date-pickers/desktop-date-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-date-picker.json @@ -47,6 +47,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/desktop-date-range-picker.json b/docs/translations/api-docs/date-pickers/desktop-date-range-picker.json index fcb403f421994..5a3224897efef 100644 --- a/docs/translations/api-docs/date-pickers/desktop-date-range-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-date-range-picker.json @@ -47,6 +47,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "value": "The selected value. Used when the component is controlled.", "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." }, 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 dc964f4f2a309..726f9612f8084 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 @@ -59,6 +59,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/desktop-time-picker.json b/docs/translations/api-docs/date-pickers/desktop-time-picker.json index b9595c59407d6..127f3a2f8d3bd 100644 --- a/docs/translations/api-docs/date-pickers/desktop-time-picker.json +++ b/docs/translations/api-docs/date-pickers/desktop-time-picker.json @@ -41,6 +41,7 @@ "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "thresholdToRenderTimeInASingleColumn": "Amount of time options below or at which the single column time renderer is used.", "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/digital-clock.json b/docs/translations/api-docs/date-pickers/digital-clock.json index d3e4551ff6a9d..ea2298693ff7c 100644 --- a/docs/translations/api-docs/date-pickers/digital-clock.json +++ b/docs/translations/api-docs/date-pickers/digital-clock.json @@ -27,6 +27,7 @@ "slots": "Overrideable component slots.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "timeStep": "The time steps between two time options. For example, if timeStep = 45, then the available time options will be [00:00, 00:45, 01:30, 02:15, 03:00, etc.].", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", "views": "Available views." diff --git a/docs/translations/api-docs/date-pickers/mobile-date-picker.json b/docs/translations/api-docs/date-pickers/mobile-date-picker.json index 39cc63d8ccdd1..5fdd17ca62a05 100644 --- a/docs/translations/api-docs/date-pickers/mobile-date-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-date-picker.json @@ -47,6 +47,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/mobile-date-range-picker.json b/docs/translations/api-docs/date-pickers/mobile-date-range-picker.json index 701ec35c9c46d..6fab0218e5957 100644 --- a/docs/translations/api-docs/date-pickers/mobile-date-range-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-date-range-picker.json @@ -47,6 +47,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "value": "The selected value. Used when the component is controlled.", "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." }, diff --git a/docs/translations/api-docs/date-pickers/mobile-date-time-picker.json b/docs/translations/api-docs/date-pickers/mobile-date-time-picker.json index b2cd4eea330fc..9204c67dda1cc 100644 --- a/docs/translations/api-docs/date-pickers/mobile-date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-date-time-picker.json @@ -57,6 +57,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/mobile-time-picker.json b/docs/translations/api-docs/date-pickers/mobile-time-picker.json index a10cee3d339dd..9b3aa443fa86b 100644 --- a/docs/translations/api-docs/date-pickers/mobile-time-picker.json +++ b/docs/translations/api-docs/date-pickers/mobile-time-picker.json @@ -38,6 +38,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/month-calendar.json b/docs/translations/api-docs/date-pickers/month-calendar.json index 511157b8d8a72..c2be234553e7b 100644 --- a/docs/translations/api-docs/date-pickers/month-calendar.json +++ b/docs/translations/api-docs/date-pickers/month-calendar.json @@ -11,11 +11,12 @@ "maxDate": "Maximal selectable date.", "minDate": "Minimal selectable date.", "monthsPerRow": "Months rendered per row.", - "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate | null) => void
value: The new value.", + "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate) => void
value: The new value.", "readOnly": "If true picker is readonly", "referenceDate": "The date used to generate the new value when both value and defaultValue are empty.", "shouldDisableMonth": "Disable specific month.

Signature:
function(month: TDate) => boolean
month: The month to test.
returns (boolean): If true, the month will be disabled.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "value": "The selected value. Used when the component is controlled." }, "classDescriptions": { "root": { "description": "Styles applied to the root element." } }, diff --git a/docs/translations/api-docs/date-pickers/multi-input-date-range-field.json b/docs/translations/api-docs/date-pickers/multi-input-date-range-field.json index ce721e6ca1e8d..7dd09ad10a31b 100644 --- a/docs/translations/api-docs/date-pickers/multi-input-date-range-field.json +++ b/docs/translations/api-docs/date-pickers/multi-input-date-range-field.json @@ -25,6 +25,7 @@ "slots": "Overridable component slots.", "spacing": "Defines the space between immediate children.", "sx": "The system prop, which allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "useFlexGap": "If true, the CSS flexbox gap is used instead of applying margin to children.
While CSS gap removes the known limitations, it is not fully supported in some browsers. We recommend checking https://caniuse.com/?search=flex%20gap before using this flag.
To enable this flag globally, follow the theme's default props configuration.", "value": "The selected value. Used when the component is controlled." }, diff --git a/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field.json b/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field.json index 9bc7e1c4edce7..3bb84bcd76f54 100644 --- a/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/multi-input-date-time-range-field.json @@ -34,6 +34,7 @@ "slots": "Overridable component slots.", "spacing": "Defines the space between immediate children.", "sx": "The system prop, which allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "useFlexGap": "If true, the CSS flexbox gap is used instead of applying margin to children.
While CSS gap removes the known limitations, it is not fully supported in some browsers. We recommend checking https://caniuse.com/?search=flex%20gap before using this flag.
To enable this flag globally, follow the theme's default props configuration.", "value": "The selected value. Used when the component is controlled." }, diff --git a/docs/translations/api-docs/date-pickers/multi-input-time-range-field.json b/docs/translations/api-docs/date-pickers/multi-input-time-range-field.json index 888f58160dc00..59a6a9c486590 100644 --- a/docs/translations/api-docs/date-pickers/multi-input-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/multi-input-time-range-field.json @@ -29,6 +29,7 @@ "slots": "Overridable slots.", "spacing": "Defines the space between immediate children.", "sx": "The system prop, which allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "useFlexGap": "If true, the CSS flexbox gap is used instead of applying margin to children.
While CSS gap removes the known limitations, it is not fully supported in some browsers. We recommend checking https://caniuse.com/?search=flex%20gap before using this flag.
To enable this flag globally, follow the theme's default props configuration.", "value": "The selected value. Used when the component is controlled." }, 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 217ba989e2124..fde75dbdadceb 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 @@ -27,6 +27,7 @@ "slots": "Overrideable 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].", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", "views": "Available views." diff --git a/docs/translations/api-docs/date-pickers/single-input-date-range-field.json b/docs/translations/api-docs/date-pickers/single-input-date-range-field.json index b6cc30d08e16f..b4e9256527953 100644 --- a/docs/translations/api-docs/date-pickers/single-input-date-range-field.json +++ b/docs/translations/api-docs/date-pickers/single-input-date-range-field.json @@ -39,6 +39,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "unstableFieldRef": "The ref object used to imperatively interact with the field.", "value": "The selected value. Used when the component is controlled.", "variant": "The variant to use." diff --git a/docs/translations/api-docs/date-pickers/single-input-date-time-range-field.json b/docs/translations/api-docs/date-pickers/single-input-date-time-range-field.json index ea5c2438f3810..bb2b5d2902bc8 100644 --- a/docs/translations/api-docs/date-pickers/single-input-date-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/single-input-date-time-range-field.json @@ -48,6 +48,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "unstableFieldRef": "The ref object used to imperatively interact with the field.", "value": "The selected value. Used when the component is controlled.", "variant": "The variant to use." diff --git a/docs/translations/api-docs/date-pickers/single-input-time-range-field.json b/docs/translations/api-docs/date-pickers/single-input-time-range-field.json index 51fbd8eae2f23..fcacca70028d0 100644 --- a/docs/translations/api-docs/date-pickers/single-input-time-range-field.json +++ b/docs/translations/api-docs/date-pickers/single-input-time-range-field.json @@ -43,6 +43,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "unstableFieldRef": "The ref object used to imperatively interact with the field.", "value": "The selected value. Used when the component is controlled.", "variant": "The variant to use." diff --git a/docs/translations/api-docs/date-pickers/static-date-picker.json b/docs/translations/api-docs/date-pickers/static-date-picker.json index b19625eb1dd63..00c029c5de78e 100644 --- a/docs/translations/api-docs/date-pickers/static-date-picker.json +++ b/docs/translations/api-docs/date-pickers/static-date-picker.json @@ -38,6 +38,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/static-date-range-picker.json b/docs/translations/api-docs/date-pickers/static-date-range-picker.json index 75f1a06919c10..fba23292071fe 100644 --- a/docs/translations/api-docs/date-pickers/static-date-range-picker.json +++ b/docs/translations/api-docs/date-pickers/static-date-range-picker.json @@ -38,6 +38,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "value": "The selected value. Used when the component is controlled.", "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." }, diff --git a/docs/translations/api-docs/date-pickers/static-date-time-picker.json b/docs/translations/api-docs/date-pickers/static-date-time-picker.json index ff5b28374d199..5b2187ebbf2b2 100644 --- a/docs/translations/api-docs/date-pickers/static-date-time-picker.json +++ b/docs/translations/api-docs/date-pickers/static-date-time-picker.json @@ -48,6 +48,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/static-time-picker.json b/docs/translations/api-docs/date-pickers/static-time-picker.json index 5562584cadb9c..f97706928f392 100644 --- a/docs/translations/api-docs/date-pickers/static-time-picker.json +++ b/docs/translations/api-docs/date-pickers/static-time-picker.json @@ -29,6 +29,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/time-clock.json b/docs/translations/api-docs/date-pickers/time-clock.json index 30a0928eda130..bcce1ef33b7f3 100644 --- a/docs/translations/api-docs/date-pickers/time-clock.json +++ b/docs/translations/api-docs/date-pickers/time-clock.json @@ -26,6 +26,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", "views": "Available views." diff --git a/docs/translations/api-docs/date-pickers/time-field.json b/docs/translations/api-docs/date-pickers/time-field.json index 51fbd8eae2f23..fcacca70028d0 100644 --- a/docs/translations/api-docs/date-pickers/time-field.json +++ b/docs/translations/api-docs/date-pickers/time-field.json @@ -43,6 +43,7 @@ "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "unstableFieldRef": "The ref object used to imperatively interact with the field.", "value": "The selected value. Used when the component is controlled.", "variant": "The variant to use." diff --git a/docs/translations/api-docs/date-pickers/time-picker.json b/docs/translations/api-docs/date-pickers/time-picker.json index 9a59c1244b73e..0020fac014837 100644 --- a/docs/translations/api-docs/date-pickers/time-picker.json +++ b/docs/translations/api-docs/date-pickers/time-picker.json @@ -42,6 +42,7 @@ "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", "thresholdToRenderTimeInASingleColumn": "Amount of time options below or at which the single column time renderer is used.", "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.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "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.", diff --git a/docs/translations/api-docs/date-pickers/year-calendar.json b/docs/translations/api-docs/date-pickers/year-calendar.json index f02ba4c4bca36..00f5172ed486d 100644 --- a/docs/translations/api-docs/date-pickers/year-calendar.json +++ b/docs/translations/api-docs/date-pickers/year-calendar.json @@ -10,11 +10,12 @@ "disablePast": "If true, disable values before the current date for date components, time for time components and both for date time components.", "maxDate": "Maximal selectable date.", "minDate": "Minimal selectable date.", - "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate | null) => void
value: The new value.", + "onChange": "Callback fired when the value changes.

Signature:
function(value: TDate) => void
value: The new value.", "readOnly": "If true picker is readonly", "referenceDate": "The date used to generate the new value when both value and defaultValue are empty.", "shouldDisableYear": "Disable specific year.

Signature:
function(year: TDate) => boolean
year: The year to test.
returns (boolean): If true, the year will be disabled.", "sx": "The system prop that allows defining system overrides as well as additional CSS styles. See the `sx` page for more details.", + "timezone": "Choose which timezone to use for the value. Example: "default", "system", "UTC", "America/New_York". If you pass values from other timezones to some props, they will be converted to this timezone before being used. See the timezones documention for more details.", "value": "The selected value. Used when the component is controlled.", "yearsPerRow": "Years rendered per row." }, diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx index e26a6c5168f7f..c38b28db2d094 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import useEventCallback from '@mui/utils/useEventCallback'; -import useControlled from '@mui/utils/useControlled'; import useMediaQuery from '@mui/material/useMediaQuery'; import { resolveComponentProps } from '@mui/base/utils'; import { styled, useThemeProps } from '@mui/material/styles'; @@ -30,6 +29,7 @@ import { UncapitalizeObjectKeys, DEFAULT_DESKTOP_MODE_MEDIA_QUERY, buildWarning, + useControlledValueWithTimezone, } from '@mui/x-date-pickers/internals'; import { getReleaseInfo } from '../internal/utils/releaseInfo'; import { @@ -158,10 +158,6 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( inProps: DateRangeCalendarProps, ref: React.Ref, ) { - const utils = useUtils(); - const localeText = useLocaleText(); - const now = useNow(); - const props = useDateRangeCalendarDefaultizedProps(inProps, 'MuiDateRangeCalendar'); const shouldHavePreview = useMediaQuery(DEFAULT_DESKTOP_MODE_MEDIA_QUERY, { defaultMatches: false, @@ -201,18 +197,26 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( fixedWeekNumber, disableDragEditing, displayWeekNumber, + timezone: timezoneProp, ...other } = props; - const slots = innerSlots ?? uncapitalizeObjectKeys(components); - const slotProps = innerSlotProps ?? componentsProps; - const [value, setValue] = useControlled>({ - controlled: valueProp, - default: defaultValue ?? rangeValueManager.emptyValue, + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ name: 'DateRangeCalendar', - state: 'value', + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager: rangeValueManager, }); + const utils = useUtils(); + const localeText = useLocaleText(); + const now = useNow(timezone); + + const slots = innerSlots ?? uncapitalizeObjectKeys(components); + const slotProps = innerSlotProps ?? componentsProps; + const { rangePosition, onRangePositionChange } = useRangePosition({ rangePosition: rangePositionProp, defaultRangePosition: inDefaultRangePosition, @@ -242,9 +246,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( onRangePositionChange(nextSelection); const isFullRangeSelected = rangePosition === 'end' && isRangeValid(utils, newRange); - - setValue(newRange); - onChange?.(newRange, isFullRangeSelected ? 'finish' : 'partial'); + handleValueChange(newRange, isFullRangeSelected ? 'finish' : 'partial'); }, ); @@ -270,6 +272,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( onDatePositionChange: handleDatePositionChange, utils, dateRange: valueDayRange, + timezone, }); const ownerState = { ...props, isDragging }; @@ -317,6 +320,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( onMonthChange, reduceAnimations, shouldDisableDate: wrappedShouldDisableDate, + timezone, }); const prevValue = React.useRef | null>(null); @@ -365,11 +369,13 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( const isNextMonthDisabled = useNextMonthDisabled(calendarState.currentMonth, { disableFuture, maxDate, + timezone, }); const isPreviousMonthDisabled = usePreviousMonthDisabled(calendarState.currentMonth, { disablePast, minDate, + timezone, }); const baseDateValidationProps: Required> = { @@ -531,6 +537,7 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar( reduceAnimations={reduceAnimations} slots={slots} slotProps={slotProps} + timezone={timezone} /> ) : ( ( autoFocus={month === focusedMonth} fixedWeekNumber={fixedWeekNumber} displayWeekNumber={displayWeekNumber} + timezone={timezone} /> ))} @@ -767,6 +775,14 @@ DateRangeCalendar.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.types.ts b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.types.ts index 1ad10d5f10115..9636c23f0ae42 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.types.ts +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.types.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import { SxProps } from '@mui/system'; import { SlotComponentProps } from '@mui/base'; import { Theme } from '@mui/material/styles'; +import { TimezoneProps } from '@mui/x-date-pickers/models'; import { BaseDateValidationProps, DefaultizedProps, @@ -51,6 +52,7 @@ export interface ExportedDateRangeCalendarProps extends ExportedDayCalendarProps, BaseDateValidationProps, DayRangeValidationProps, + TimezoneProps, // TODO: Add the other props of `ExportedUseViewOptions` once `DateRangeCalendar` handles several views Pick, 'autoFocus'> { /** diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/useDragRange.ts b/packages/x-date-pickers-pro/src/DateRangeCalendar/useDragRange.ts index f829d5397aeb5..f54a70d2b0352 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/useDragRange.ts +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/useDragRange.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { MuiPickersAdapter } from '@mui/x-date-pickers/models'; +import { MuiPickersAdapter, PickersTimezone } from '@mui/x-date-pickers/models'; import { DateRangePosition } from './DateRangeCalendar.types'; import { DateRange } from '../internal/models'; import { isEndOfRange, isStartOfRange } from '../internal/utils/date-utils'; @@ -14,6 +14,7 @@ interface UseDragRangeParams { onDatePositionChange: (position: DateRangePosition) => void; onDrop: (newDate: TDate) => void; dateRange: DateRange; + timezone: PickersTimezone; } interface UseDragRangeEvents { @@ -34,13 +35,17 @@ interface UseDragRangeResponse extends UseDragRangeEvents { draggingDatePosition: DateRangePosition | null; } -const resolveDateFromTarget = (target: EventTarget, utils: MuiPickersAdapter) => { +const resolveDateFromTarget = ( + target: EventTarget, + utils: MuiPickersAdapter, + timezone: PickersTimezone, +) => { const timestampString = (target as HTMLElement).dataset.timestamp; if (!timestampString) { return null; } const timestamp = +timestampString; - return utils.date(new Date(timestamp)); + return utils.dateWithTimezone(new Date(timestamp).toISOString(), timezone); }; const isSameAsDraggingDate = (event: React.DragEvent) => { @@ -91,6 +96,7 @@ const useDragRangeEvents = ({ onDrop, disableDragEditing, dateRange, + timezone, }: UseDragRangeParams): UseDragRangeEvents => { const emptyDragImgRef = React.useRef(null); React.useEffect(() => { @@ -113,7 +119,7 @@ const useDragRangeEvents = ({ }; const handleDragStart = useEventCallback((event: React.DragEvent) => { - const newDate = resolveDateFromTarget(event.target, utils); + const newDate = resolveDateFromTarget(event.target, utils, timezone); if (!isElementDraggable(newDate)) { return; } @@ -140,7 +146,7 @@ const useDragRangeEvents = ({ return; } - const newDate = resolveDateFromTarget(target, utils); + const newDate = resolveDateFromTarget(target, utils, timezone); if (!isElementDraggable(newDate)) { return; } @@ -162,7 +168,7 @@ const useDragRangeEvents = ({ event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'move'; - setRangeDragDay(resolveDateFromTarget(event.target, utils)); + setRangeDragDay(resolveDateFromTarget(event.target, utils, timezone)); }); const handleTouchMove = useEventCallback((event: React.TouchEvent) => { @@ -171,7 +177,7 @@ const useDragRangeEvents = ({ return; } - const newDate = resolveDateFromTarget(target, utils); + const newDate = resolveDateFromTarget(target, utils, timezone); if (newDate) { setRangeDragDay(newDate); } @@ -211,7 +217,7 @@ const useDragRangeEvents = ({ // make sure the focused element is the element where touch ended target.focus(); - const newDate = resolveDateFromTarget(target, utils); + const newDate = resolveDateFromTarget(target, utils, timezone); if (newDate) { onDrop(newDate); } @@ -242,7 +248,7 @@ const useDragRangeEvents = ({ if (isSameAsDraggingDate(event)) { return; } - const newDate = resolveDateFromTarget(event.target, utils); + const newDate = resolveDateFromTarget(event.target, utils, timezone); if (newDate) { onDrop(newDate); } @@ -267,6 +273,7 @@ export const useDragRange = ({ onDatePositionChange, onDrop, dateRange, + timezone, }: Omit< UseDragRangeParams, 'setRangeDragDay' | 'setIsDragging' | 'isDragging' @@ -302,6 +309,7 @@ export const useDragRange = ({ setRangeDragDay: handleRangeDragDayChange, disableDragEditing, dateRange, + timezone, }); return React.useMemo( diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx index 038eae8e14ddf..f5251358a27ef 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DateRangePicker/DateRangePicker.tsx @@ -325,6 +325,14 @@ DateRangePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx index b80fd67bf8088..24b880ed08822 100644 --- a/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/DesktopDateRangePicker/DesktopDateRangePicker.tsx @@ -355,6 +355,14 @@ DesktopDateRangePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx index 884359c5d3c83..a55219e8c9a57 100644 --- a/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/MobileDateRangePicker/MobileDateRangePicker.tsx @@ -355,6 +355,14 @@ MobileDateRangePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx index 3cea1ae730b8a..42da9f6f26471 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateRangeField/MultiInputDateRangeField.tsx @@ -344,6 +344,14 @@ MultiInputDateRangeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, unstableEndFieldRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), unstableStartFieldRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx index bafcf78240f38..64060e0d370cf 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.tsx @@ -393,6 +393,14 @@ MultiInputDateTimeRangeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, unstableEndFieldRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), unstableStartFieldRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** diff --git a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts index 9316d38c81a24..665fcdc2082eb 100644 --- a/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/MultiInputDateTimeRangeField/MultiInputDateTimeRangeField.types.ts @@ -97,4 +97,5 @@ export interface MultiInputDateTimeRangeFieldSlotsComponentsProps { export type UseMultiInputDateTimeRangeFieldDefaultizedProps< TDate, AdditionalProps extends {}, -> = UseDateTimeRangeFieldDefaultizedProps & AdditionalProps; +> = UseDateTimeRangeFieldDefaultizedProps & + Omit; diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx index bc80029041190..a3db6dc1ed8eb 100644 --- a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.tsx @@ -370,6 +370,14 @@ MultiInputTimeRangeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, unstableEndFieldRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), unstableStartFieldRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), /** diff --git a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts index cdcd99635a924..fd8800b7f12db 100644 --- a/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/MultiInputTimeRangeField/MultiInputTimeRangeField.types.ts @@ -92,4 +92,5 @@ export interface MultiInputTimeRangeFieldSlotsComponentsProps { export type UseMultiInputTimeRangeFieldDefaultizedProps< TDate, AdditionalProps extends {}, -> = UseTimeRangeFieldDefaultizedProps & AdditionalProps; +> = UseTimeRangeFieldDefaultizedProps & + Omit; diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx index 207b792d1dde7..36bed8f03f0e6 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.tsx @@ -311,6 +311,14 @@ SingleInputDateRangeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The ref object used to imperatively interact with the field. */ diff --git a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts index c679bb62185d8..e9cbf105b6ee8 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts +++ b/packages/x-date-pickers-pro/src/SingleInputDateRangeField/SingleInputDateRangeField.types.ts @@ -14,7 +14,8 @@ export interface UseSingleInputDateRangeFieldProps extends UseDateRangeFi export type UseSingleInputDateRangeFieldDefaultizedProps< TDate, AdditionalProps extends {}, -> = UseDateRangeFieldDefaultizedProps & AdditionalProps; +> = UseDateRangeFieldDefaultizedProps & + Omit; export type UseSingleInputDateRangeFieldComponentProps = Omit< TChildProps, diff --git a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx index e563a4f010725..dd8dad66a1a6a 100644 --- a/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputDateTimeRangeField/SingleInputDateTimeRangeField.tsx @@ -361,6 +361,14 @@ SingleInputDateTimeRangeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The ref object used to imperatively interact with the field. */ diff --git a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx index 5d3c87fa34ce3..89d48f1811fdc 100644 --- a/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx +++ b/packages/x-date-pickers-pro/src/SingleInputTimeRangeField/SingleInputTimeRangeField.tsx @@ -336,6 +336,14 @@ SingleInputTimeRangeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The ref object used to imperatively interact with the field. */ diff --git a/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx b/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx index c8325f6140bb5..f7b6193df33c1 100644 --- a/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/StaticDateRangePicker/StaticDateRangePicker.tsx @@ -276,6 +276,14 @@ StaticDateRangePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx b/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx index 2e14497c14005..4a6869ea23252 100644 --- a/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx +++ b/packages/x-date-pickers-pro/src/dateRangeViewRenderers/dateRangeViewRenderers.tsx @@ -49,6 +49,7 @@ export const renderDateRangeViewCalendar = ({ fixedWeekNumber, disableDragEditing, displayWeekNumber, + timezone, }: DateRangeViewRendererProps) => ( ({ fixedWeekNumber={fixedWeekNumber} disableDragEditing={disableDragEditing} displayWeekNumber={displayWeekNumber} + timezone={timezone} /> ); diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx b/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx index d42353021b6f7..045d1aa552f4c 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internal/hooks/useDesktopRangePicker/useDesktopRangePicker.tsx @@ -46,6 +46,7 @@ export const useDesktopRangePicker = < sx, format, formatDensity, + timezone, label, inputRef, readOnly, @@ -117,6 +118,7 @@ export const useDesktopRangePicker = < sx, format, formatDensity, + timezone, autoFocus: autoFocus && !props.open, ref: fieldContainerRef, ...(fieldType === 'single-input' && { inputRef }), diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx index f93b68777e02f..59231d7011a9b 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMobileRangePicker/useMobileRangePicker.tsx @@ -45,6 +45,7 @@ export const useMobileRangePicker = < sx, format, formatDensity, + timezone, label, inputRef, readOnly, @@ -100,6 +101,7 @@ export const useMobileRangePicker = < sx, format, formatDensity, + timezone, ...(fieldType === 'single-input' && { inputRef }), }, ownerState: props, diff --git a/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts index 56e087bcae598..a6c72af7cdb9c 100644 --- a/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/internal/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts @@ -1,4 +1,3 @@ -import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; import { unstable_useDateField as useDateField, @@ -12,9 +11,9 @@ import { FieldChangeHandler, FieldChangeHandlerContext, UseFieldResponse, + useControlledValueWithTimezone, } from '@mui/x-date-pickers/internals'; import { DateValidationError } from '@mui/x-date-pickers/models'; -import useControlled from '@mui/utils/useControlled'; import { useDefaultizedDateRangeFieldProps } from '../../../SingleInputDateRangeField/useSingleInputDateRangeField'; import { UseMultiInputDateRangeFieldParams } from '../../../MultiInputDateRangeField/MultiInputDateRangeField.types'; import { DateRange } from '../../models/range'; @@ -54,14 +53,16 @@ export const useMultiInputDateRangeField = >({ + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ name: 'useMultiInputDateRangeField', - state: 'value', - controlled: valueProp, - default: firstDefaultValue.current ?? rangeValueManager.emptyValue, + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager: rangeValueManager, }); // TODO: Maybe export utility from `useField` instead of copy/pasting the logic @@ -72,18 +73,16 @@ export const useMultiInputDateRangeField = = index === 0 ? [newDate, value[1]] : [value[0], newDate]; - setValue(newDateRange); - const context: FieldChangeHandlerContext = { ...rawContext, validationError: validateDateRange({ adapter, value: newDateRange, - props: sharedProps, + props: { ...sharedProps, timezone }, }), }; - onChange?.(newDateRange, context); + handleValueChange(newDateRange, context); }; }; @@ -96,7 +95,7 @@ export const useMultiInputDateRangeField = >( - { ...sharedProps, value }, + { ...sharedProps, value, timezone }, validateDateRange, rangeValueManager.isSameError, rangeValueManager.defaultErrorState, @@ -113,6 +112,7 @@ export const useMultiInputDateRangeField = >({ - name: 'useMultiInputDateTimeRangeField', - state: 'value', - controlled: valueProp, - default: firstDefaultValue.current ?? rangeValueManager.emptyValue, + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ + name: 'useMultiInputDateRangeField', + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager: rangeValueManager, }); // TODO: Maybe export utility from `useField` instead of copy/pasting the logic @@ -99,18 +100,16 @@ export const useMultiInputDateTimeRangeField = = index === 0 ? [newDate, value[1]] : [value[0], newDate]; - setValue(newDateRange); - const context: FieldChangeHandlerContext = { ...rawContext, validationError: validateDateTimeRange({ adapter, value: newDateRange, - props: sharedProps, + props: { ...sharedProps, timezone }, }), }; - onChange?.(newDateRange, context); + handleValueChange(newDateRange, context); }; }; @@ -123,7 +122,7 @@ export const useMultiInputDateTimeRangeField = >( - { ...sharedProps, value }, + { ...sharedProps, value, timezone }, validateDateTimeRange, rangeValueManager.isSameError, rangeValueManager.defaultErrorState, @@ -139,6 +138,7 @@ export const useMultiInputDateTimeRangeField = >({ - name: 'useMultiInputTimeRangeField', - state: 'value', - controlled: valueProp, - default: firstDefaultValue.current ?? rangeValueManager.emptyValue, + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ + name: 'useMultiInputDateRangeField', + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager: rangeValueManager, }); // TODO: Maybe export utility from `useField` instead of copy/pasting the logic @@ -89,18 +90,16 @@ export const useMultiInputTimeRangeField = = index === 0 ? [newDate, value[1]] : [value[0], newDate]; - setValue(newDateRange); - const context: FieldChangeHandlerContext = { ...rawContext, validationError: validateTimeRange({ adapter, value: newDateRange, - props: sharedProps, + props: { ...sharedProps, timezone }, }), }; - onChange?.(newDateRange, context); + handleValueChange(newDateRange, context); }; }; @@ -113,7 +112,7 @@ export const useMultiInputTimeRangeField = ( - { ...sharedProps, value }, + { ...sharedProps, value, timezone }, validateTimeRange, rangeValueManager.isSameError, rangeValueManager.defaultErrorState, @@ -129,6 +128,7 @@ export const useMultiInputTimeRangeField = extends DayRangeValidationProps, - Required> {} + Required>, + DefaultizedProps {} export const validateDateRange: Validator< DateRange, diff --git a/packages/x-date-pickers-pro/src/internal/utils/validation/validateDateTimeRange.ts b/packages/x-date-pickers-pro/src/internal/utils/validation/validateDateTimeRange.ts index dbb18295856e6..b4f46b7eee278 100644 --- a/packages/x-date-pickers-pro/src/internal/utils/validation/validateDateTimeRange.ts +++ b/packages/x-date-pickers-pro/src/internal/utils/validation/validateDateTimeRange.ts @@ -1,8 +1,10 @@ +import { TimezoneProps } from '@mui/x-date-pickers/models'; import { Validator, validateDateTime, BaseDateValidationProps, TimeValidationProps, + DefaultizedProps, } from '@mui/x-date-pickers/internals'; import { isRangeValid } from '../date-utils'; import { DayRangeValidationProps } from '../../models/dateRange'; @@ -12,7 +14,8 @@ import { DateTimeRangeValidationError } from '../../../models'; export interface DateTimeRangeComponentValidationProps extends DayRangeValidationProps, TimeValidationProps, - Required> {} + Required>, + DefaultizedProps {} export const validateDateTimeRange: Validator< DateRange, diff --git a/packages/x-date-pickers-pro/src/internal/utils/validation/validateTimeRange.ts b/packages/x-date-pickers-pro/src/internal/utils/validation/validateTimeRange.ts index 129a77c4b0beb..7815be5f8b814 100644 --- a/packages/x-date-pickers-pro/src/internal/utils/validation/validateTimeRange.ts +++ b/packages/x-date-pickers-pro/src/internal/utils/validation/validateTimeRange.ts @@ -1,9 +1,17 @@ -import { Validator, validateTime, BaseTimeValidationProps } from '@mui/x-date-pickers/internals'; +import { TimezoneProps } from '@mui/x-date-pickers/models'; +import { + Validator, + validateTime, + BaseTimeValidationProps, + DefaultizedProps, +} from '@mui/x-date-pickers/internals'; import { isRangeValid } from '../date-utils'; import { DateRange } from '../../models/range'; import { TimeRangeValidationError } from '../../../models'; -export interface TimeRangeComponentValidationProps extends Required {} +export interface TimeRangeComponentValidationProps + extends Required, + DefaultizedProps {} export const validateTimeRange: Validator< DateRange, diff --git a/packages/x-date-pickers-pro/src/internal/utils/valueManagers.ts b/packages/x-date-pickers-pro/src/internal/utils/valueManagers.ts index 46c97f1208393..74ee06a3c721e 100644 --- a/packages/x-date-pickers-pro/src/internal/utils/valueManagers.ts +++ b/packages/x-date-pickers-pro/src/internal/utils/valueManagers.ts @@ -28,9 +28,9 @@ export type RangePickerValueManager< export const rangeValueManager: RangePickerValueManager = { emptyValue: [null, null], - getTodayValue: (utils, valueType) => [ - getTodayDate(utils, valueType), - getTodayDate(utils, valueType), + getTodayValue: (utils, timezone, valueType) => [ + getTodayDate(utils, timezone, valueType), + getTodayDate(utils, timezone, valueType), ], getInitialReferenceValue: ({ value, referenceDate: referenceDateProp, ...params }) => { const shouldKeepStartDate = value[0] != null && params.utils.isValid(value[0]); @@ -64,6 +64,10 @@ export const rangeValueManager: RangePickerValueManager = { return timezoneStart ?? timezoneEnd; }, + setTimezone: (utils, timezone, value) => [ + value[0] == null ? null : utils.setTimezone(value[0], timezone), + value[1] == null ? null : utils.setTimezone(value[1], timezone), + ], }; export const rangeFieldValueManager: FieldValueManager, any, RangeFieldSection> = { diff --git a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx index 45531426313de..2b14b1f2b43cb 100644 --- a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx +++ b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { spy } from 'sinon'; -import dayjs from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; @@ -28,15 +28,18 @@ import 'dayjs/plugin/utc'; import 'dayjs/plugin/timezone'; describe('', () => { - describeGregorianAdapter(AdapterDayjs, { + const commonParams = { formatDateTime: 'YYYY-MM-DD HH:mm:ss', setDefaultTimezone: dayjs.tz.setDefault, + getLocaleFromDate: (value: Dayjs) => value.locale(), frenchLocale: 'fr', - }); + }; + + describeGregorianAdapter(AdapterDayjs, commonParams); // Makes sure that all the tests that do not use timezones works fine when dayjs do not support UTC / timezone. describeGregorianAdapter(AdapterDayjs, { - formatDateTime: 'YYYY-MM-DD HH:mm:ss', + ...commonParams, prepareAdapter: (adapter) => { // @ts-ignore adapter.hasUTCPlugin = () => false; @@ -45,13 +48,11 @@ describe('', () => { // Makes sure that we don't run timezone related tests, that would not work. adapter.isTimezoneCompatible = false; }, - setDefaultTimezone: dayjs.tz.setDefault, - frenchLocale: 'fr', }); describe('Adapter localization', () => { describe('English', () => { - const adapter = new AdapterDayjs({ instance: dayjs, locale: 'en' }); + const adapter = new AdapterDayjs({ locale: 'en' }); const date = adapter.date(TEST_DATE_ISO_STRING)!; it('getWeekdays: should start on Sunday', () => { @@ -70,7 +71,7 @@ describe('', () => { }); describe('Russian', () => { - const adapter = new AdapterDayjs({ instance: dayjs, locale: 'ru' }); + const adapter = new AdapterDayjs({ locale: 'ru' }); it('getWeekDays: should start on Monday', () => { const result = adapter.getWeekdays(); diff --git a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts index 2b2605e996743..07d42f216559a 100644 --- a/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts +++ b/packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts @@ -149,7 +149,7 @@ export class AdapterDayjs implements MuiPickersAdapter { public lib = 'dayjs'; - public rawDayJsInstance: typeof defaultDayjs; + public rawDayJsInstance?: typeof defaultDayjs; public dayjs: Constructor; @@ -162,8 +162,8 @@ export class AdapterDayjs implements MuiPickersAdapter { public formatTokenMap = formatTokenMap; constructor({ locale, formats, instance }: AdapterOptions = {}) { - this.rawDayJsInstance = instance || defaultDayjs; - this.dayjs = withLocale(this.rawDayJsInstance, locale); + this.rawDayJsInstance = instance; + this.dayjs = withLocale(this.rawDayJsInstance ?? defaultDayjs, locale); this.locale = locale; this.formats = { ...defaultFormats, ...formats }; @@ -189,9 +189,31 @@ export class AdapterDayjs implements MuiPickersAdapter { return value.format(comparisonTemplate) === comparingInValueTimezone.format(comparisonTemplate); }; + /** + * Replace "default" by undefined before passing it to `dayjs + */ + private cleanTimezone = (timezone: string) => (timezone === 'default' ? undefined : timezone); + private createSystemDate = (value: string | undefined): Dayjs => { // TODO v7: Stop using `this.rawDayJsInstance` (drop the `instance` param on the adapters) - return this.rawDayJsInstance(value); + /* istanbul ignore next */ + if (this.rawDayJsInstance) { + return this.rawDayJsInstance(value); + } + + if (this.hasUTCPlugin() && this.hasTimezonePlugin()) { + const timezone = defaultDayjs.tz.guess(); + + // We can't change the system timezone in the tests + /* istanbul ignore next */ + if (timezone !== 'UTC') { + return defaultDayjs.tz(value, timezone); + } + + return defaultDayjs(value); + } + + return defaultDayjs(value); }; private createUTCDate = (value: string | undefined): Dayjs => { @@ -214,10 +236,8 @@ export class AdapterDayjs implements MuiPickersAdapter { throw new Error(MISSING_TIMEZONE_PLUGIN); } - const cleanTimezone = timezone === 'default' ? undefined : timezone; const keepLocalTime = value !== undefined && !value.endsWith('Z'); - - return defaultDayjs(value).tz(cleanTimezone, keepLocalTime); + return defaultDayjs(value).tz(this.cleanTimezone(timezone), keepLocalTime); }; private getLocaleFormats = () => { @@ -296,12 +316,11 @@ export class AdapterDayjs implements MuiPickersAdapter { return value.utc(); } + // We know that we have the UTC plugin. + // Otherwise, the value timezone would always equal "system". + // And it would be caught by the first "if" of this method. if (timezone === 'system') { - if (this.hasUTCPlugin()) { - return value.local(); - } - - return value; + return value.local(); } if (!this.hasTimezonePlugin()) { @@ -313,9 +332,7 @@ export class AdapterDayjs implements MuiPickersAdapter { throw new Error(MISSING_TIMEZONE_PLUGIN); } - const cleanZone = timezone === 'default' ? undefined : timezone; - - return defaultDayjs.tz(value, cleanZone); + return defaultDayjs.tz(value, this.cleanTimezone(timezone)); }; public toJsDate = (value: Dayjs) => { @@ -403,7 +420,7 @@ export class AdapterDayjs implements MuiPickersAdapter { return true; } - return this.dayjs(value).isSame(comparing); + return this.dayjs(value).toDate().getTime() === this.dayjs(comparing).toDate().getTime(); }; public isSameYear = (value: Dayjs, comparing: Dayjs) => { @@ -616,9 +633,11 @@ export class AdapterDayjs implements MuiPickersAdapter { }; public getWeekArray = (value: Dayjs) => { - const cleanLocale = this.setLocaleToValue(value); - const start = cleanLocale.startOf('month').startOf('week'); - const end = cleanLocale.endOf('month').endOf('week'); + const timezone = this.getTimezone(value); + + const cleanValue = this.setLocaleToValue(value); + const start = cleanValue.startOf('month').startOf('week'); + const end = cleanValue.endOf('month').endOf('week'); let count = 0; let current = start; @@ -630,6 +649,15 @@ export class AdapterDayjs implements MuiPickersAdapter { nestedWeeks[weekNumber].push(current); current = current.add(1, 'day'); + + // If the new day does not have the same offset as the old one (when switching to summer day time for example), + // Then dayjs will not automatically adjust the offset (moment does) + // We have to parse again the value to make sure the `fixOffset` method is applied + // See https://github.com/iamkun/dayjs/blob/b3624de619d6e734cd0ffdbbd3502185041c1b60/src/plugin/timezone/index.js#L72 + if (this.hasTimezonePlugin() && timezone !== 'UTC' && timezone !== 'system') { + current = current.tz(this.cleanTimezone(timezone), true); + } + count += 1; } diff --git a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx index 086472d152866..78d94572319bc 100644 --- a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx +++ b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Settings } from 'luxon'; +import { DateTime, Settings } from 'luxon'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { AdapterFormats } from '@mui/x-date-pickers/models'; @@ -22,6 +22,7 @@ describe('', () => { setDefaultTimezone: (timezone) => { Settings.defaultZone = timezone ?? 'system'; }, + getLocaleFromDate: (value: DateTime) => value.locale!, frenchLocale: 'fr', }); diff --git a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts index d6aeeb3e57e8b..66bbedd9feff2 100644 --- a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts +++ b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts @@ -134,6 +134,15 @@ export class AdapterLuxon implements MuiPickersAdapter { this.formats = { ...defaultFormats, ...formats }; } + private setLocaleToValue = (value: DateTime) => { + const expectedLocale = this.getCurrentLocaleCode(); + if (expectedLocale === value.locale) { + return value; + } + + return value.setLocale(expectedLocale); + }; + public date = (value?: any) => { if (typeof value === 'undefined') { return DateTime.local(); @@ -179,7 +188,7 @@ export class AdapterLuxon implements MuiPickersAdapter { return 'system'; } - return value.zoneName ?? 'system'; + return value.zoneName!; }; public setTimezone = (value: DateTime, timezone: PickersTimezone): DateTime => { @@ -199,7 +208,7 @@ export class AdapterLuxon implements MuiPickersAdapter { }; public toISO = (value: DateTime) => { - return value.toISO({ format: 'extended' })!; + return value.toUTC().toISO({ format: 'extended' })!; }; public parse = (value: string, formatString: string) => { @@ -517,17 +526,18 @@ export class AdapterLuxon implements MuiPickersAdapter { }; public getWeekArray = (value: DateTime) => { - const { days } = value + const cleanValue = this.setLocaleToValue(value); + const { days } = cleanValue .endOf('month') .endOf('week') - .diff(value.startOf('month').startOf('week'), 'days') + .diff(cleanValue.startOf('month').startOf('week'), 'days') .toObject(); const weeks: DateTime[][] = []; new Array(Math.round(days!)) .fill(0) .map((_, i) => i) - .map((day) => value.startOf('month').startOf('week').plus({ days: day })) + .map((day) => cleanValue.startOf('month').startOf('week').plus({ days: day })) .forEach((v, i) => { if (i === 0 || (i % 7 === 0 && i > 6)) { weeks.push([v]); diff --git a/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx b/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx index 45f728827f608..bbd38830beb3d 100644 --- a/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx +++ b/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import moment from 'moment'; +import moment, { Moment } from 'moment'; import momentTZ from 'moment-timezone'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; @@ -21,25 +21,25 @@ import { } from '@mui/x-date-pickers/tests/describeGregorianAdapter'; describe('', () => { - describeGregorianAdapter(AdapterMoment, { + const commonParams = { formatDateTime: 'YYYY-MM-DD HH:mm:ss', dateLibInstanceWithTimezoneSupport: momentTZ, setDefaultTimezone: momentTZ.tz.setDefault, + getLocaleFromDate: (value: Moment) => value.locale(), frenchLocale: 'fr', - }); + }; + + describeGregorianAdapter(AdapterMoment, commonParams); // Makes sure that all the tests that do not use timezones works fine when dayjs do not support UTC / timezone. describeGregorianAdapter(AdapterMoment, { - formatDateTime: 'YYYY-MM-DD HH:mm:ss', - dateLibInstanceWithTimezoneSupport: momentTZ, + ...commonParams, prepareAdapter: (adapter) => { // @ts-ignore adapter.hasTimezonePlugin = () => false; // Makes sure that we don't run timezone related tests, that would not work. adapter.isTimezoneCompatible = false; }, - setDefaultTimezone: momentTZ.tz.setDefault, - frenchLocale: 'fr', }); describe('Adapter localization', () => { diff --git a/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts b/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts index 14fc8fc2f1c67..34ed134f92f8c 100644 --- a/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts +++ b/packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts @@ -14,7 +14,7 @@ const formatTokenMap: FieldFormatTokenMap = { // Year Y: 'year', YY: 'year', - YYYY: 'year', + YYYY: { sectionType: 'year', contentType: 'digit', maxLength: 4 }, // Month M: { sectionType: 'month', contentType: 'digit', maxLength: 2 }, @@ -233,6 +233,10 @@ export class AdapterMoment implements MuiPickersAdapter { }; public setTimezone = (value: Moment, timezone: PickersTimezone): Moment => { + if (this.getTimezone(value) === timezone) { + return value; + } + if (timezone === 'UTC') { return value.clone().utc(); } @@ -242,12 +246,12 @@ export class AdapterMoment implements MuiPickersAdapter { } if (!this.hasTimezonePlugin()) { - if (timezone === 'default') { - return value; + /* istanbul ignore next */ + if (timezone !== 'default') { + throw new Error(MISSING_TIMEZONE_PLUGIN); } - /* istanbul ignore next */ - throw new Error(MISSING_TIMEZONE_PLUGIN); + return value; } const cleanZone = @@ -256,6 +260,10 @@ export class AdapterMoment implements MuiPickersAdapter { this.moment.defaultZone?.name ?? 'system' : timezone; + if (cleanZone === 'system') { + return value.clone().local(); + } + const newValue = value.clone(); newValue.tz(cleanZone); @@ -562,9 +570,9 @@ export class AdapterMoment implements MuiPickersAdapter { }; public getWeekArray = (value: Moment) => { - const cleanLocale = this.setLocaleToValue(value); - const start = cleanLocale.clone().startOf('month').startOf('week'); - const end = cleanLocale.clone().endOf('month').endOf('week'); + const cleanValue = this.setLocaleToValue(value); + const start = cleanValue.clone().startOf('month').startOf('week'); + const end = cleanValue.clone().endOf('month').endOf('week'); let count = 0; let current = start; diff --git a/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx b/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx index 4c7328f325d2e..d96becf08a19a 100644 --- a/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx +++ b/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx @@ -6,7 +6,6 @@ import { unstable_composeClasses as composeClasses, unstable_useId as useId, unstable_useEventCallback as useEventCallback, - unstable_useControlled as useControlled, } from '@mui/utils'; import { DateCalendarProps, DateCalendarDefaultizedProps } from './DateCalendar.types'; import { useCalendarState } from './useCalendarState'; @@ -26,7 +25,8 @@ import { PickerViewRoot } from '../internals/components/PickerViewRoot'; import { defaultReduceAnimations } from '../internals/utils/defaultReduceAnimations'; import { getDateCalendarUtilityClass } from './dateCalendarClasses'; import { BaseDateValidationProps } from '../internals/models/validation'; -import type { PickerSelectionState } from '../internals/hooks/usePicker'; +import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; +import { singleItemValueManager } from '../internals/utils/valueManagers'; const useUtilityClasses = (ownerState: DateCalendarProps) => { const { classes } = ownerState; @@ -139,23 +139,19 @@ export const DateCalendar = React.forwardRef(function DateCalendar( displayWeekNumber, yearsPerRow, monthsPerRow, + timezone: timezoneProp, ...other } = props; - const [value, setValue] = useControlled({ + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ name: 'DateCalendar', - state: 'value', - controlled: valueProp, - default: defaultValue ?? null, + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager: singleItemValueManager, }); - const handleValueChange = useEventCallback( - (newValue: TDate | null, selectionState?: PickerSelectionState) => { - setValue(newValue); - onChange?.(newValue, selectionState); - }, - ); - const { view, setView, focusedView, setFocusedView, goToNextView, setValueAndGoToNextView } = useViews({ view: inView, @@ -187,6 +183,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar( shouldDisableDate, disablePast, disableFuture, + timezone, }); const handleDateMonthChange = useEventCallback((newDate: TDate) => { @@ -202,6 +199,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar( disablePast, disableFuture, isDateDisabled, + timezone, }) : newDate; @@ -229,6 +227,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar( disablePast, disableFuture, isDateDisabled, + timezone, }) : newDate; @@ -276,6 +275,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar( disableHighlightToday, readOnly, disabled, + timezone, }; const gridLabelId = `${id}-grid-label`; @@ -319,6 +319,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar( labelId={gridLabelId} slots={slots} slotProps={slotProps} + timezone={timezone} /> BaseDateValidationProps, DayValidationProps, YearValidationProps, - MonthValidationProps { + MonthValidationProps, + TimezoneProps { /** * Default calendar month displayed when `value` and `defaultValue` are empty. */ diff --git a/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx b/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx index db983231f0ae0..a256dff17dcc3 100644 --- a/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx +++ b/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx @@ -27,6 +27,8 @@ import { useIsDateDisabled } from './useIsDateDisabled'; import { findClosestEnabledDate } from '../internals/utils/date-utils'; import { DayCalendarClasses, getDayCalendarUtilityClass } from './dayCalendarClasses'; import { SlotsAndSlotProps } from '../internals/utils/slots-migration'; +import { TimezoneProps } from '../models'; +import { DefaultizedProps } from '../internals/models/helpers'; export interface DayCalendarSlotsComponent { /** @@ -83,6 +85,7 @@ export interface DayCalendarProps MonthValidationProps, YearValidationProps, Required>, + DefaultizedProps, SlotsAndSlotProps, DayCalendarSlotsComponentsProps> { autoFocus?: boolean; className?: string; @@ -234,9 +237,6 @@ function WrappedDay({ currentMonthNumber: number; isViewFocused: boolean; }) { - const utils = useUtils(); - const now = useNow(); - const { disabled, disableHighlightToday, @@ -246,8 +246,12 @@ function WrappedDay({ componentsProps, slots, slotProps, + timezone, } = parentProps; + const utils = useUtils(); + const now = useNow(timezone); + const isFocusableDay = focusableDay !== null && utils.isSameDay(day, focusableDay); const isSelected = selectedDays.some((selectedDay) => utils.isSameDay(selectedDay, day)); const isToday = utils.isSameDay(day, now); @@ -317,12 +321,7 @@ function WrappedDay({ * @ignore - do not document. */ export function DayCalendar(inProps: DayCalendarProps) { - const now = useNow(); - const utils = useUtils(); const props = useThemeProps({ props: inProps, name: 'MuiDayCalendar' }); - const classes = useUtilityClasses(props); - const theme = useTheme(); - const isRTL = theme.direction === 'rtl'; const { onFocusedDayChange, @@ -352,8 +351,15 @@ export function DayCalendar(inProps: DayCalendarProps) { displayWeekNumber, fixedWeekNumber, autoFocus, + timezone, } = props; + const now = useNow(timezone); + const utils = useUtils(); + const classes = useUtilityClasses(props); + const theme = useTheme(); + const isRTL = theme.direction === 'rtl'; + const isDateDisabled = useIsDateDisabled({ shouldDisableDate, shouldDisableMonth, @@ -362,6 +368,7 @@ export function DayCalendar(inProps: DayCalendarProps) { maxDate, disablePast, disableFuture, + timezone, }); const localeText = useLocaleText(); @@ -416,6 +423,7 @@ export function DayCalendar(inProps: DayCalendarProps) { minDate: isRTL ? newFocusedDayDefault : utils.startOfMonth(nextAvailableMonth), maxDate: isRTL ? utils.endOfMonth(nextAvailableMonth) : newFocusedDayDefault, isDateDisabled, + timezone, }); focusDay(closestDayToFocus || newFocusedDayDefault); event.preventDefault(); @@ -431,6 +439,7 @@ export function DayCalendar(inProps: DayCalendarProps) { minDate: isRTL ? utils.startOfMonth(nextAvailableMonth) : newFocusedDayDefault, maxDate: isRTL ? newFocusedDayDefault : utils.endOfMonth(nextAvailableMonth), isDateDisabled, + timezone, }); focusDay(closestDayToFocus || newFocusedDayDefault); event.preventDefault(); @@ -495,14 +504,24 @@ export function DayCalendar(inProps: DayCalendarProps) { disablePast, disableFuture, isDateDisabled, + timezone, }); } return internalFocusedDay; - }, [currentMonth, disableFuture, disablePast, internalFocusedDay, isDateDisabled, utils]); + }, [ + currentMonth, + disableFuture, + disablePast, + internalFocusedDay, + isDateDisabled, + utils, + timezone, + ]); const weeksToDisplay = React.useMemo(() => { - const toDisplay = utils.getWeekArray(currentMonth); - let nextMonth = utils.addMonths(currentMonth, 1); + const currentMonthWithTimezone = utils.setTimezone(currentMonth, timezone); + const toDisplay = utils.getWeekArray(currentMonthWithTimezone); + let nextMonth = utils.addMonths(currentMonthWithTimezone, 1); while (fixedWeekNumber && toDisplay.length < fixedWeekNumber) { const additionalWeeks = utils.getWeekArray(nextMonth); const hasCommonWeek = utils.isSameDay( @@ -519,7 +538,7 @@ export function DayCalendar(inProps: DayCalendarProps) { nextMonth = utils.addMonths(nextMonth, 1); } return toDisplay; - }, [currentMonth, fixedWeekNumber, utils]); + }, [currentMonth, fixedWeekNumber, utils, timezone]); return (
diff --git a/packages/x-date-pickers/src/DateCalendar/PickersCalendarHeader.tsx b/packages/x-date-pickers/src/DateCalendar/PickersCalendarHeader.tsx index 56d6183b7a8fb..9cc7c890a4c85 100644 --- a/packages/x-date-pickers/src/DateCalendar/PickersCalendarHeader.tsx +++ b/packages/x-date-pickers/src/DateCalendar/PickersCalendarHeader.tsx @@ -192,6 +192,7 @@ export function PickersCalendarHeader(inProps: PickersCalendarHeaderProps reduceAnimations, views, labelId, + timezone, } = props; const ownerState = props; @@ -225,10 +226,12 @@ export function PickersCalendarHeader(inProps: PickersCalendarHeaderProps const isNextMonthDisabled = useNextMonthDisabled(month, { disableFuture, maxDate, + timezone, }); const isPreviousMonthDisabled = usePreviousMonthDisabled(month, { disablePast, minDate, + timezone, }); const handleToggleView = () => { diff --git a/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx b/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx new file mode 100644 index 0000000000000..355fb1b2b4726 --- /dev/null +++ b/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { spy } from 'sinon'; +import { expect } from 'chai'; +import { userEvent, screen } from '@mui/monorepo/test/utils'; +import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; +import { describeAdapters } from '@mui/x-date-pickers/tests/describeAdapters'; + +const TIMEZONE_TO_TEST = ['UTC', 'system', 'America/New_York']; + +describe(' - Timezone', () => { + describeAdapters('Timezone prop', DateCalendar, ({ adapter, render }) => { + if (!adapter.isTimezoneCompatible) { + return; + } + + it('should use default timezone for rendering and onChange when no value and no timezone prop are provided', () => { + const onChange = spy(); + render(); + + userEvent.mousePress(screen.getByRole('gridcell', { name: '25' })); + const expectedDate = adapter.setDate(adapter.dateWithTimezone(undefined, 'default')!, 25); + + // Check the `onChange` value (uses default timezone, e.g: UTC, see TZ env variable) + const actualDate = onChange.lastCall.firstArg; + + // On dayjs, we are not able to know if a date is UTC because it's the system timezone or because it was created as UTC. + // In a real world scenario, this should probably never occur. + expect(adapter.getTimezone(actualDate)).to.equal(adapter.lib === 'dayjs' ? 'UTC' : 'system'); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + TIMEZONE_TO_TEST.forEach((timezone) => { + describe(`Timezone: ${timezone}`, () => { + it('should use timezone prop for onChange when no value is provided', () => { + const onChange = spy(); + render(); + userEvent.mousePress(screen.getByRole('gridcell', { name: '25' })); + const expectedDate = adapter.setDate( + adapter.startOfDay(adapter.dateWithTimezone(undefined, timezone)!), + 25, + ); + + // Check the `onChange` value (uses timezone prop) + const actualDate = onChange.lastCall.firstArg; + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + it('should use value timezone for onChange when a value is provided', () => { + const onChange = spy(); + const value = adapter.dateWithTimezone('2022-04-25T15:30', timezone)!; + + render(); + + userEvent.mousePress(screen.getByRole('gridcell', { name: '25' })); + const expectedDate = adapter.setDate(value, 25); + + // Check the `onChange` value (uses timezone prop) + const actualDate = onChange.lastCall.firstArg; + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + }); + }); + }); +}); diff --git a/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx b/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx index 0393acedbea37..1754dc47f8ac6 100644 --- a/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx +++ b/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx @@ -3,7 +3,7 @@ import useEventCallback from '@mui/utils/useEventCallback'; import { SlideDirection } from './PickersSlideTransition'; import { useIsDateDisabled } from './useIsDateDisabled'; import { useUtils, useNow } from '../internals/hooks/useUtils'; -import { MuiPickersAdapter } from '../models'; +import { MuiPickersAdapter, PickersTimezone } from '../models'; import { DateCalendarDefaultizedProps } from './DateCalendar.types'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { SECTION_TYPE_GRANULARITY } from '../internals/utils/getDefaultReferenceDate'; @@ -108,6 +108,7 @@ interface UseCalendarStateParams | 'shouldDisableDate' > { disableSwitchToMonthOnDayFocus?: boolean; + timezone: PickersTimezone; } export const useCalendarState = (params: UseCalendarStateParams) => { @@ -123,9 +124,10 @@ export const useCalendarState = (params: UseCalendarState onMonthChange, reduceAnimations, shouldDisableDate, + timezone, } = params; - const now = useNow(); + const now = useNow(timezone); const utils = useUtils(); const reducerFn = React.useRef( @@ -149,6 +151,7 @@ export const useCalendarState = (params: UseCalendarState return singleItemValueManager.getInitialReferenceValue({ value, utils, + timezone, props: params, referenceDate: externalReferenceDate, granularity: SECTION_TYPE_GRANULARITY.day, @@ -201,6 +204,7 @@ export const useCalendarState = (params: UseCalendarState maxDate, disableFuture, disablePast, + timezone, }); const onMonthSwitchingAnimationEnd = React.useCallback(() => { diff --git a/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts b/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts index b4623ff27146c..0375516f4e953 100644 --- a/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts +++ b/packages/x-date-pickers/src/DateCalendar/useIsDateDisabled.ts @@ -13,6 +13,7 @@ export const useIsDateDisabled = ({ maxDate, disableFuture, disablePast, + timezone, }: DateComponentValidationProps) => { const adapter = useLocalizationContext(); @@ -29,6 +30,7 @@ export const useIsDateDisabled = ({ maxDate, disableFuture, disablePast, + timezone, }, }) !== null, [ @@ -40,6 +42,7 @@ export const useIsDateDisabled = ({ maxDate, disableFuture, disablePast, + timezone, ], ); }; diff --git a/packages/x-date-pickers/src/DateField/DateField.tsx b/packages/x-date-pickers/src/DateField/DateField.tsx index 2069b266a5cd3..942d1a9e8d2b5 100644 --- a/packages/x-date-pickers/src/DateField/DateField.tsx +++ b/packages/x-date-pickers/src/DateField/DateField.tsx @@ -321,6 +321,14 @@ DateField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The ref object used to imperatively interact with the field. */ diff --git a/packages/x-date-pickers/src/DatePicker/DatePicker.tsx b/packages/x-date-pickers/src/DatePicker/DatePicker.tsx index 147f321416d10..c5eaed502360d 100644 --- a/packages/x-date-pickers/src/DatePicker/DatePicker.tsx +++ b/packages/x-date-pickers/src/DatePicker/DatePicker.tsx @@ -328,6 +328,14 @@ DatePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx b/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx index 13822efbfade7..3ef27b157d1da 100644 --- a/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx +++ b/packages/x-date-pickers/src/DateTimeField/DateTimeField.tsx @@ -372,6 +372,14 @@ DateTimeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The ref object used to imperatively interact with the field. */ diff --git a/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx b/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx index 8ab92fc934db7..e3a76ee09479e 100644 --- a/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx +++ b/packages/x-date-pickers/src/DateTimeField/tests/timezone.DateTimeField.test.tsx @@ -1,10 +1,119 @@ import * as React from 'react'; +import { spy } from 'sinon'; +import { expect } from 'chai'; +import { userEvent } from '@mui/monorepo/test/utils'; import { DateTimeField } from '@mui/x-date-pickers/DateTimeField'; import { createPickerRenderer, expectInputValue, getTextbox } from 'test/utils/pickers-utils'; +import { describeAdapters } from '@mui/x-date-pickers/tests/describeAdapters'; + +const TIMEZONE_TO_TEST = ['UTC', 'system', 'America/New_York']; describe(' - Timezone', () => { - describe('Value time-zone modification - Luxon', () => { + describeAdapters('Timezone prop', DateTimeField, ({ adapter, render, clickOnInput }) => { + if (!adapter.isTimezoneCompatible) { + return; + } + + const format = `${adapter.formats.keyboardDate} ${adapter.formats.hours24h}`; + + const fillEmptyValue = (input: HTMLInputElement, timezone: string) => { + clickOnInput(input, 0); + + // Set month + userEvent.keyPress(input, { key: 'ArrowDown' }); + userEvent.keyPress(input, { key: 'ArrowRight' }); + + // Set day + userEvent.keyPress(input, { key: 'ArrowDown' }); + userEvent.keyPress(input, { key: 'ArrowRight' }); + + // Set year + userEvent.keyPress(input, { key: 'ArrowDown' }); + userEvent.keyPress(input, { key: 'ArrowRight' }); + + // Set hours + userEvent.keyPress(input, { key: 'ArrowDown' }); + userEvent.keyPress(input, { key: 'ArrowRight' }); + + return adapter.setHours( + adapter.setDate(adapter.setMonth(adapter.dateWithTimezone(undefined, timezone)!, 11), 31), + 23, + ); + }; + + it('should use default timezone for rendering and onChange when no value and no timezone prop are provided', () => { + if (adapter.lib !== 'dayjs') { + return; + } + + const onChange = spy(); + render(); + + const input = getTextbox(); + const expectedDate = fillEmptyValue(input, 'default'); + + // Check the rendered value (uses default timezone, e.g: UTC, see TZ env variable) + expectInputValue(input, '12/31/2022 23'); + + // Check the `onChange` value (uses default timezone, e.g: UTC, see TZ env variable) + const actualDate = onChange.lastCall.firstArg; + + // On dayjs, we are not able to know if a date is UTC because it's the system timezone or because it was created as UTC. + // In a real world scenario, this should probably never occur. + expect(adapter.getTimezone(actualDate)).to.equal(adapter.lib === 'dayjs' ? 'UTC' : 'system'); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + TIMEZONE_TO_TEST.forEach((timezone) => { + describe(`Timezone: ${timezone}`, () => { + it('should use timezone prop for onChange and rendering when no value is provided', () => { + const onChange = spy(); + render(); + const input = getTextbox(); + const expectedDate = fillEmptyValue(input, timezone); + + // Check the rendered value (uses timezone prop) + expectInputValue(input, '12/31/2022 23'); + + // Check the `onChange` value (uses timezone prop) + const actualDate = onChange.lastCall.firstArg; + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + it('should use timezone prop for rendering and value timezone for onChange when a value is provided', () => { + const onChange = spy(); + render( + , + ); + const input = getTextbox(); + clickOnInput(input, 0); + userEvent.keyPress(input, { key: 'ArrowDown' }); + + // Check the rendered value (uses America/Chicago timezone) + expectInputValue(input, '05/14/2022 19'); + + // Check the `onChange` value (uses timezone prop) + const expectedDate = adapter.addMonths( + adapter.dateWithTimezone(undefined, timezone)!, + -1, + ); + const actualDate = onChange.lastCall.firstArg; + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + }); + }); + }); + + describe('Value timezone modification - Luxon', () => { const { render, adapter } = createPickerRenderer({ clock: 'fake', adapterName: 'luxon' }); + it('should update the field when time zone changes (timestamp remains the same)', () => { const { setProps } = render(); const input = getTextbox(); diff --git a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx index a909cf6212742..09bf0a08923ac 100644 --- a/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx +++ b/packages/x-date-pickers/src/DateTimePicker/DateTimePicker.tsx @@ -398,6 +398,14 @@ DateTimePicker.propTypes = { minutes: PropTypes.number, seconds: PropTypes.number, }), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx index 743eabb4e0925..fccc63a094567 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePicker.tsx @@ -367,6 +367,14 @@ DesktopDatePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx index f95b58083506c..b278f88bacc0a 100644 --- a/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopDateTimePicker/DesktopDateTimePicker.tsx @@ -479,6 +479,14 @@ DesktopDateTimePicker.propTypes = { minutes: PropTypes.number, seconds: PropTypes.number, }), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx index e81cfafe8305e..d38a009efe0f6 100644 --- a/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx +++ b/packages/x-date-pickers/src/DesktopTimePicker/DesktopTimePicker.tsx @@ -372,6 +372,14 @@ DesktopTimePicker.propTypes = { minutes: PropTypes.number, seconds: PropTypes.number, }), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx b/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx index c5de1361cfc1f..589b88e5b47af 100644 --- a/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx +++ b/packages/x-date-pickers/src/DigitalClock/DigitalClock.tsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import { alpha, styled, useThemeProps } from '@mui/material/styles'; import useEventCallback from '@mui/utils/useEventCallback'; import composeClasses from '@mui/utils/composeClasses'; -import useControlled from '@mui/utils/useControlled'; import MenuItem from '@mui/material/MenuItem'; import MenuList from '@mui/material/MenuList'; import useForkRef from '@mui/utils/useForkRef'; @@ -16,6 +15,8 @@ import { DigitalClockProps } from './DigitalClock.types'; import { useViews } from '../internals/hooks/useViews'; 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'; const useUtilityClasses = (ownerState: DigitalClockProps) => { const { classes } = ownerState; @@ -84,11 +85,10 @@ export const DigitalClock = React.forwardRef(function DigitalClock, ref: React.Ref, ) { - const now = useNow(); const utils = useUtils(); + const containerRef = React.useRef(null); const handleRef = useForkRef(ref, containerRef); - const localeText = useLocaleText(); const props = useThemeProps({ props: inProps, @@ -124,9 +124,26 @@ export const DigitalClock = React.forwardRef(function DigitalClock(); + const now = useNow(timezone); + const ownerState = React.useMemo( () => ({ ...props, alreadyRendered: !!containerRef.current }), [props], @@ -137,17 +154,9 @@ export const DigitalClock = React.forwardRef(function DigitalClock { - setValue(newValue); - onChange?.(newValue, 'finish'); - }); + const handleValueChange = useEventCallback((newValue: TDate | null) => + handleRawValueChange(newValue, 'finish'), + ); const { setValueAndGoToNextView } = useViews>({ view: inView, @@ -446,6 +455,14 @@ DigitalClock.propTypes = { * @default 30 */ timeStep: PropTypes.number, + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/DigitalClock/tests/timezone.DigitalClock.test.tsx b/packages/x-date-pickers/src/DigitalClock/tests/timezone.DigitalClock.test.tsx new file mode 100644 index 0000000000000..cd743e21c828c --- /dev/null +++ b/packages/x-date-pickers/src/DigitalClock/tests/timezone.DigitalClock.test.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { spy } from 'sinon'; +import { expect } from 'chai'; +import { screen, userEvent } from '@mui/monorepo/test/utils'; +import { DigitalClock } from '@mui/x-date-pickers/DigitalClock'; +import { describeAdapters } from '@mui/x-date-pickers/tests/describeAdapters'; +import { getDateOffset } from 'test/utils/pickers-utils'; + +const TIMEZONE_TO_TEST = ['UTC', 'system', 'America/New_York']; + +const get24HourFromDigitalClock = () => { + const results = /([0-9]+):([0-9]+) (AM|PM)/.exec( + screen.queryByRole('option', { selected: true })!.innerHTML, + )!; + + return Number(results[1]) + (results[3] === 'AM' ? 0 : 12); +}; + +describe(' - Timezone', () => { + describeAdapters('Timezone prop', DigitalClock, ({ adapter, render }) => { + if (!adapter.isTimezoneCompatible) { + return; + } + + it('should use default timezone for rendering and onChange when no value and no timezone prop are provided', () => { + const onChange = spy(); + render(); + + userEvent.mousePress(screen.getByRole('option', { name: '08:00 AM' })); + + const expectedDate = adapter.setHours(adapter.dateWithTimezone(undefined, 'default')!, 8); + + // Check the `onChange` value (uses default timezone, e.g: UTC, see TZ env variable) + const actualDate = onChange.lastCall.firstArg; + + // On dayjs, we are not able to know if a date is UTC because it's the system timezone or because it was created as UTC. + // In a real world scenario, this should probably never occur. + expect(adapter.getTimezone(actualDate)).to.equal(adapter.lib === 'dayjs' ? 'UTC' : 'system'); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + TIMEZONE_TO_TEST.forEach((timezone) => { + describe(`Timezone: ${timezone}`, () => { + it('should use timezone prop for onChange when no value is provided', () => { + const onChange = spy(); + render(); + + userEvent.mousePress(screen.getByRole('option', { name: '08:00 AM' })); + + const expectedDate = adapter.setHours( + adapter.startOfDay(adapter.dateWithTimezone(undefined, timezone)!), + 8, + ); + + // Check the `onChange` value (uses timezone prop) + const actualDate = onChange.lastCall.firstArg; + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + it('should use timezone prop for rendering and value timezone for onChange when a value is provided', () => { + const onChange = spy(); + const value = adapter.dateWithTimezone('2022-04-17T04:30', timezone); + + render( + , + ); + + const renderedHourBefore = get24HourFromDigitalClock(); + + const offsetDiff = + getDateOffset(adapter, adapter.setTimezone(value, 'America/Chicago')) - + getDateOffset(adapter, value); + + expect(renderedHourBefore).to.equal( + (adapter.getHours(value) + offsetDiff / 60 + 24) % 24, + ); + + userEvent.mousePress(screen.getByRole('option', { name: '08:30 PM' })); + + const actualDate = onChange.lastCall.firstArg; + + const renderedHourAfter = get24HourFromDigitalClock(); + expect(renderedHourAfter).to.equal( + (adapter.getHours(actualDate) + offsetDiff / 60 + 24) % 24, + ); + + const expectedDate = adapter.addHours(value, renderedHourAfter - renderedHourBefore); + + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + }); + }); + }); +}); diff --git a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx index 435fe21403b92..f972536c9fde2 100644 --- a/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx +++ b/packages/x-date-pickers/src/MobileDatePicker/MobileDatePicker.tsx @@ -363,6 +363,14 @@ MobileDatePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx index 972d7e8991986..e4ce6c78281e4 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.tsx @@ -431,6 +431,14 @@ MobileDateTimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx index f05f372d7d17a..dcf59cad4c389 100644 --- a/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx +++ b/packages/x-date-pickers/src/MobileTimePicker/MobileTimePicker.tsx @@ -315,6 +315,14 @@ MobileTimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx index 8cf8f7f19ebd7..b2430bb5dadb9 100644 --- a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx +++ b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.tsx @@ -16,6 +16,7 @@ import { DefaultizedProps } from '../internals/models/helpers'; import { MonthCalendarProps } from './MonthCalendar.types'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { SECTION_TYPE_GRANULARITY } from '../internals/utils/getDefaultReferenceDate'; +import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; const useUtilityClasses = (ownerState: MonthCalendarProps) => { const { classes } = ownerState; @@ -70,12 +71,7 @@ export const MonthCalendar = React.forwardRef(function MonthCalendar( inProps: MonthCalendarProps, ref: React.Ref, ) { - const now = useNow(); - const theme = useTheme(); - const utils = useUtils(); - const props = useMonthCalendarDefaultizedProps(inProps, 'MuiMonthCalendar'); - const { className, value: valueProp, @@ -95,31 +91,39 @@ export const MonthCalendar = React.forwardRef(function MonthCalendar( hasFocus, onFocusedViewChange, monthsPerRow = 3, + timezone: timezoneProp, ...other } = props; - const ownerState = props; - const classes = useUtilityClasses(ownerState); - - const [value, setValue] = useControlled({ + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ name: 'MonthCalendar', - state: 'value', - controlled: valueProp, - default: defaultValue ?? null, + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange: onChange as (value: TDate | null) => void, + valueManager: singleItemValueManager, }); + const now = useNow(timezone); + const theme = useTheme(); + const utils = useUtils(); + const referenceDate = React.useMemo( () => singleItemValueManager.getInitialReferenceValue({ value, utils, props, + timezone, referenceDate: referenceDateProp, granularity: SECTION_TYPE_GRANULARITY.month, }), [], // eslint-disable-line react-hooks/exhaustive-deps ); + const ownerState = props; + const classes = useUtilityClasses(ownerState); + const todayMonth = React.useMemo(() => utils.getMonth(now), [utils, now]); const selectedMonth = React.useMemo(() => { @@ -185,8 +189,7 @@ export const MonthCalendar = React.forwardRef(function MonthCalendar( } const newDate = utils.setMonth(value ?? referenceDate, month); - setValue(newDate); - onChange?.(newDate); + handleValueChange(newDate); }); const focusMonth = useEventCallback((month: number) => { @@ -336,7 +339,7 @@ MonthCalendar.propTypes = { /** * Callback fired when the value changes. * @template TDate - * @param {TDate | null} value The new value. + * @param {TDate} value The new value. */ onChange: PropTypes.func, onFocusedViewChange: PropTypes.func, @@ -365,6 +368,14 @@ MonthCalendar.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts index 2b9a9039fa3e4..6de34938e659f 100644 --- a/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts +++ b/packages/x-date-pickers/src/MonthCalendar/MonthCalendar.types.ts @@ -2,6 +2,7 @@ import { SxProps } from '@mui/system'; import { Theme } from '@mui/material/styles'; import { MonthCalendarClasses } from './monthCalendarClasses'; import { BaseDateValidationProps, MonthValidationProps } from '../internals/models/validation'; +import { TimezoneProps } from '../models'; export interface ExportedMonthCalendarProps { /** @@ -13,7 +14,8 @@ export interface ExportedMonthCalendarProps { export interface MonthCalendarProps extends ExportedMonthCalendarProps, MonthValidationProps, - BaseDateValidationProps { + BaseDateValidationProps, + TimezoneProps { autoFocus?: boolean; /** * className applied to the root element. @@ -47,7 +49,7 @@ export interface MonthCalendarProps /** * Callback fired when the value changes. * @template TDate - * @param {TDate | null} value The new value. + * @param {TDate} value The new value. */ onChange?: (value: TDate) => void; /** If `true` picker is readonly */ diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx index 6095ab3f19470..88662a6423eb7 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import { styled, useThemeProps } from '@mui/material/styles'; import useEventCallback from '@mui/utils/useEventCallback'; import composeClasses from '@mui/utils/composeClasses'; -import useControlled from '@mui/utils/useControlled'; import { useUtils, useNow, useLocaleText } from '../internals/hooks/useUtils'; import { convertValueToMeridiem, createIsAfterIgnoreDatePart } from '../internals/utils/time-utils'; import { useViews } from '../internals/hooks/useViews'; @@ -20,6 +19,8 @@ import { import { getHourSectionOptions, getTimeSectionOptions } from './MultiSectionDigitalClock.utils'; import { TimeStepOptions, TimeView } from '../models'; import { TimeViewWithMeridiem } from '../internals/models'; +import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; +import { singleItemValueManager } from '../internals/utils/valueManagers'; const useUtilityClasses = (ownerState: MultiSectionDigitalClockProps) => { const { classes } = ownerState; @@ -48,9 +49,7 @@ type MultiSectionDigitalClockComponent = (( export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDigitalClock< TDate extends unknown, >(inProps: MultiSectionDigitalClockProps, ref: React.Ref) { - const now = useNow(); const utils = useUtils(); - const localeText = useLocaleText(); const props = useThemeProps({ props: inProps, @@ -86,8 +85,26 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi disabled, readOnly, skipDisabled = false, + timezone: timezoneProp, ...other } = props; + + const { + value, + handleValueChange: handleRawValueChange, + timezone, + } = useControlledValueWithTimezone({ + name: 'MultiSectionDigitalClock', + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager: singleItemValueManager, + }); + + const localeText = useLocaleText(); + const now = useNow(timezone); + const timeSteps = React.useMemo>( () => ({ hours: 1, @@ -98,22 +115,12 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi [inTimeSteps], ); - const [value, setValue] = useControlled({ - name: 'MultiSectionDigitalClock', - state: 'value', - controlled: valueProp, - default: defaultValue ?? null, - }); - const handleValueChange = useEventCallback( ( newValue: TDate | null, selectionState?: PickerSelectionState, selectedView?: TimeViewWithMeridiem, - ) => { - setValue(newValue); - onChange?.(newValue, selectionState, selectedView); - }, + ) => handleRawValueChange(newValue, selectionState, selectedView), ); const views = React.useMemo(() => { @@ -561,6 +568,14 @@ MultiSectionDigitalClock.propTypes = { minutes: PropTypes.number, seconds: PropTypes.number, }), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx b/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx index 6fb6bde74cb30..1c2a148c981bc 100644 --- a/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx +++ b/packages/x-date-pickers/src/StaticDatePicker/StaticDatePicker.tsx @@ -280,6 +280,14 @@ StaticDatePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx index 961c8f263dba2..848f48029269b 100644 --- a/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx +++ b/packages/x-date-pickers/src/StaticDateTimePicker/StaticDateTimePicker.tsx @@ -346,6 +346,14 @@ StaticDateTimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx index dd6e8b27c4b80..1216c83527952 100644 --- a/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx +++ b/packages/x-date-pickers/src/StaticTimePicker/StaticTimePicker.tsx @@ -229,6 +229,14 @@ StaticTimePicker.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx index e074df8b0a510..b39556d5617d2 100644 --- a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx +++ b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx @@ -2,12 +2,7 @@ import * as React from 'react'; import clsx from 'clsx'; import PropTypes from 'prop-types'; import { styled, useThemeProps } from '@mui/material/styles'; -import { - unstable_composeClasses as composeClasses, - unstable_useControlled as useControlled, - unstable_useId as useId, -} from '@mui/utils'; -import useEventCallback from '@mui/utils/useEventCallback'; +import { unstable_composeClasses as composeClasses, unstable_useId as useId } from '@mui/utils'; import { useUtils, useNow, useLocaleText } from '../internals/hooks/useUtils'; import { PickersArrowSwitcher } from '../internals/components/PickersArrowSwitcher'; import { convertValueToMeridiem, createIsAfterIgnoreDatePart } from '../internals/utils/time-utils'; @@ -20,6 +15,8 @@ import { getTimeClockUtilityClass } from './timeClockClasses'; import { Clock, ClockProps } from './Clock'; import { TimeClockProps } from './TimeClock.types'; import { getHourNumbers, getMinutesNumbers } from './ClockNumbers'; +import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; +import { singleItemValueManager } from '../internals/utils/valueManagers'; import { uncapitalizeObjectKeys } from '../internals/utils/slots-migration'; const useUtilityClasses = (ownerState: TimeClockProps) => { @@ -66,8 +63,6 @@ export const TimeClock = React.forwardRef(function TimeClock, ref: React.Ref, ) { - const localeText = useLocaleText(); - const now = useNow(); const utils = useUtils(); const props = useThemeProps({ @@ -104,25 +99,23 @@ export const TimeClock = React.forwardRef(function TimeClock { - setValue(newValue); - onChange?.(newValue, selectionState); - }, - ); + const localeText = useLocaleText(); + const now = useNow(timezone); const { view, setView, previousView, nextView, setValueAndGoToNextView } = useViews({ view: inView, @@ -534,6 +527,14 @@ TimeClock.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/TimeClock/tests/timezone.TimeClock.test.tsx b/packages/x-date-pickers/src/TimeClock/tests/timezone.TimeClock.test.tsx new file mode 100644 index 0000000000000..87b359c794494 --- /dev/null +++ b/packages/x-date-pickers/src/TimeClock/tests/timezone.TimeClock.test.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { spy } from 'sinon'; +import { expect } from 'chai'; +import { screen, fireTouchChangedEvent } from '@mui/monorepo/test/utils'; +import { TimeClock } from '@mui/x-date-pickers/TimeClock'; +import { describeAdapters } from '@mui/x-date-pickers/tests/describeAdapters'; +import { getClockTouchEvent, getDateOffset, getTimeClockValue } from 'test/utils/pickers-utils'; + +const TIMEZONE_TO_TEST = ['UTC', 'system', 'America/New_York']; + +describe(' - Timezone', () => { + describeAdapters('Timezone prop', TimeClock, ({ adapter, render }) => { + if (!adapter.isTimezoneCompatible) { + return; + } + + it('should use default timezone for rendering and onChange when no value and no timezone prop are provided', () => { + const onChange = spy(); + render(); + + const hourClockEvent = getClockTouchEvent(8, '12hours'); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', hourClockEvent); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', hourClockEvent); + + const expectedDate = adapter.setHours(adapter.dateWithTimezone(undefined, 'default')!, 8); + + // Check the `onChange` value (uses default timezone, e.g: UTC, see TZ env variable) + const actualDate = onChange.lastCall.firstArg; + + // On dayjs, we are not able to know if a date is UTC because it's the system timezone or because it was created as UTC. + // In a real world scenario, this should probably never occur. + expect(adapter.getTimezone(actualDate)).to.equal(adapter.lib === 'dayjs' ? 'UTC' : 'system'); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + TIMEZONE_TO_TEST.forEach((timezone) => { + describe(`Timezone: ${timezone}`, () => { + it('should use timezone prop for onChange when no value is provided', () => { + const onChange = spy(); + render(); + + const hourClockEvent = getClockTouchEvent(8, '12hours'); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', hourClockEvent); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', hourClockEvent); + + const expectedDate = adapter.setHours( + adapter.startOfDay(adapter.dateWithTimezone(undefined, timezone)!), + 8, + ); + + // Check the `onChange` value (uses timezone prop) + const actualDate = onChange.lastCall.firstArg; + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + it('should use timezone prop for rendering and value timezone for onChange when a value is provided', () => { + const onChange = spy(); + const value = adapter.dateWithTimezone('2022-04-17T04:30', timezone); + + render( + , + ); + + const renderedHourBefore = getTimeClockValue(); + const offsetDiff = + getDateOffset(adapter, adapter.setTimezone(value, 'America/Chicago')) - + getDateOffset(adapter, value); + + expect(renderedHourBefore).to.equal( + (adapter.getHours(value) + offsetDiff / 60 + 12) % 12, + ); + + const hourClockEvent = getClockTouchEvent(8, '12hours'); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchmove', hourClockEvent); + fireTouchChangedEvent(screen.getByMuiTest('clock'), 'touchend', hourClockEvent); + + const actualDate = onChange.lastCall.firstArg; + + const renderedHourAfter = getTimeClockValue(); + expect(renderedHourAfter).to.equal( + (adapter.getHours(actualDate) + offsetDiff / 60 + 12) % 12, + ); + + const expectedDate = adapter.addHours(value, renderedHourAfter - renderedHourBefore); + + expect(adapter.getTimezone(actualDate)).to.equal(timezone); + expect(actualDate).toEqualDateTime(expectedDate); + }); + }); + }); + }); +}); diff --git a/packages/x-date-pickers/src/TimeField/TimeField.tsx b/packages/x-date-pickers/src/TimeField/TimeField.tsx index b62845f6ca74c..ecec153b2fe00 100644 --- a/packages/x-date-pickers/src/TimeField/TimeField.tsx +++ b/packages/x-date-pickers/src/TimeField/TimeField.tsx @@ -333,6 +333,14 @@ TimeField.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The ref object used to imperatively interact with the field. */ diff --git a/packages/x-date-pickers/src/TimePicker/TimePicker.tsx b/packages/x-date-pickers/src/TimePicker/TimePicker.tsx index e3fc8cdb32510..47e56fbbfb8a2 100644 --- a/packages/x-date-pickers/src/TimePicker/TimePicker.tsx +++ b/packages/x-date-pickers/src/TimePicker/TimePicker.tsx @@ -295,6 +295,14 @@ TimePicker.propTypes = { minutes: PropTypes.number, seconds: PropTypes.number, }), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx b/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx index 2c9159af46d12..667103c8fcf23 100644 --- a/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx +++ b/packages/x-date-pickers/src/YearCalendar/YearCalendar.tsx @@ -17,6 +17,7 @@ import { applyDefaultDate } from '../internals/utils/date-utils'; import { YearCalendarProps } from './YearCalendar.types'; import { singleItemValueManager } from '../internals/utils/valueManagers'; import { SECTION_TYPE_GRANULARITY } from '../internals/utils/getDefaultReferenceDate'; +import { useControlledValueWithTimezone } from '../internals/hooks/useValueWithTimezone'; const useUtilityClasses = (ownerState: YearCalendarProps) => { const { classes } = ownerState; @@ -74,10 +75,6 @@ export const YearCalendar = React.forwardRef(function YearCalendar( inProps: YearCalendarProps, ref: React.Ref, ) { - const now = useNow(); - const theme = useTheme(); - const utils = useUtils(); - const props = useYearCalendarDefaultizedProps(inProps, 'MuiYearCalendar'); const { autoFocus, @@ -98,31 +95,39 @@ export const YearCalendar = React.forwardRef(function YearCalendar( hasFocus, onFocusedViewChange, yearsPerRow = 3, + timezone: timezoneProp, ...other } = props; - const ownerState = props; - const classes = useUtilityClasses(ownerState); - - const [value, setValue] = useControlled({ + const { value, handleValueChange, timezone } = useControlledValueWithTimezone({ name: 'YearCalendar', - state: 'value', - controlled: valueProp, - default: defaultValue ?? null, + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange: onChange as (value: TDate | null) => void, + valueManager: singleItemValueManager, }); + const now = useNow(timezone); + const theme = useTheme(); + const utils = useUtils(); + const referenceDate = React.useMemo( () => singleItemValueManager.getInitialReferenceValue({ value, utils, props, + timezone, referenceDate: referenceDateProp, granularity: SECTION_TYPE_GRANULARITY.year, }), [], // eslint-disable-line react-hooks/exhaustive-deps ); + const ownerState = props; + const classes = useUtilityClasses(ownerState); + const todayYear = React.useMemo(() => utils.getYear(now), [utils, now]); const selectedYear = React.useMemo(() => { if (value != null) { @@ -184,8 +189,7 @@ export const YearCalendar = React.forwardRef(function YearCalendar( } const newDate = utils.setYear(value ?? referenceDate, year); - setValue(newDate); - onChange?.(newDate); + handleValueChange(newDate); }); const focusYear = useEventCallback((year: number) => { @@ -348,7 +352,7 @@ YearCalendar.propTypes = { /** * Callback fired when the value changes. * @template TDate - * @param {TDate | null} value The new value. + * @param {TDate} value The new value. */ onChange: PropTypes.func, onFocusedViewChange: PropTypes.func, @@ -377,6 +381,14 @@ YearCalendar.propTypes = { PropTypes.func, PropTypes.object, ]), + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone: PropTypes.string, /** * The selected value. * Used when the component is controlled. diff --git a/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts b/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts index 99785fa453f29..12dd8c8dae5f8 100644 --- a/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts +++ b/packages/x-date-pickers/src/YearCalendar/YearCalendar.types.ts @@ -2,6 +2,7 @@ import { SxProps } from '@mui/system'; import { Theme } from '@mui/material/styles'; import { YearCalendarClasses } from './yearCalendarClasses'; import { BaseDateValidationProps, YearValidationProps } from '../internals/models/validation'; +import { TimezoneProps } from '../models'; export interface ExportedYearCalendarProps { /** @@ -14,7 +15,8 @@ export interface ExportedYearCalendarProps { export interface YearCalendarProps extends ExportedYearCalendarProps, YearValidationProps, - BaseDateValidationProps { + BaseDateValidationProps, + TimezoneProps { autoFocus?: boolean; /** * className applied to the root element. @@ -48,7 +50,7 @@ export interface YearCalendarProps /** * Callback fired when the value changes. * @template TDate - * @param {TDate | null} value The new value. + * @param {TDate} value The new value. */ onChange?: (value: TDate) => void; /** If `true` picker is readonly */ diff --git a/packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx b/packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx index de38812ef2b9f..63fef77db26ea 100644 --- a/packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx +++ b/packages/x-date-pickers/src/dateTimeViewRenderers/dateTimeViewRenderers.tsx @@ -79,6 +79,7 @@ export const renderDesktopDateTimeView = ({ autoFocus, fixedWeekNumber, displayWeekNumber, + timezone, disableIgnoringDatePartForTimeValidation, timeSteps, skipDisabled, @@ -130,6 +131,7 @@ export const renderDesktopDateTimeView = ({ autoFocus={autoFocus} fixedWeekNumber={fixedWeekNumber} displayWeekNumber={displayWeekNumber} + timezone={timezone} /> {timeViewsCount > 0 && ( @@ -171,6 +173,7 @@ export const renderDesktopDateTimeView = ({ disableIgnoringDatePartForTimeValidation={disableIgnoringDatePartForTimeValidation} timeSteps={timeSteps} skipDisabled={skipDisabled} + timezone={timezone} /> )} diff --git a/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx b/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx index 0a6a23a645070..267d5330dd4e9 100644 --- a/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx +++ b/packages/x-date-pickers/src/dateViewRenderers/dateViewRenderers.tsx @@ -54,6 +54,7 @@ export const renderDateViewCalendar = ({ autoFocus, fixedWeekNumber, displayWeekNumber, + timezone, }: DateViewRendererProps) => ( ({ autoFocus={autoFocus} fixedWeekNumber={fixedWeekNumber} displayWeekNumber={displayWeekNumber} + timezone={timezone} /> ); diff --git a/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx b/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx index a53023ff122a9..6a2b3a4a193ef 100644 --- a/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx +++ b/packages/x-date-pickers/src/internals/hooks/date-helpers-hooks.tsx @@ -3,41 +3,51 @@ import { useUtils } from './useUtils'; import { PickerOnChangeFn } from './useViews'; import { getMeridiem, convertToMeridiem } from '../utils/time-utils'; import { PickerSelectionState } from './usePicker'; +import { PickersTimezone } from '../../models'; interface MonthValidationOptions { disablePast?: boolean; disableFuture?: boolean; minDate: TDate; maxDate: TDate; + timezone: PickersTimezone; } export function useNextMonthDisabled( month: TDate, - { disableFuture, maxDate }: Pick, 'disableFuture' | 'maxDate'>, + { + disableFuture, + maxDate, + timezone, + }: Pick, 'disableFuture' | 'maxDate' | 'timezone'>, ) { const utils = useUtils(); return React.useMemo(() => { - const now = utils.date()!; + const now = utils.dateWithTimezone(undefined, timezone)!; const lastEnabledMonth = utils.startOfMonth( disableFuture && utils.isBefore(now, maxDate) ? now : maxDate, ); return !utils.isAfter(lastEnabledMonth, month); - }, [disableFuture, maxDate, month, utils]); + }, [disableFuture, maxDate, month, utils, timezone]); } export function usePreviousMonthDisabled( month: TDate, - { disablePast, minDate }: Pick, 'disablePast' | 'minDate'>, + { + disablePast, + minDate, + timezone, + }: Pick, 'disablePast' | 'minDate' | 'timezone'>, ) { const utils = useUtils(); return React.useMemo(() => { - const now = utils.date()!; + const now = utils.dateWithTimezone(undefined, timezone)!; const firstEnabledMonth = utils.startOfMonth( disablePast && utils.isAfter(now, minDate) ? now : minDate, ); return !utils.isBefore(firstEnabledMonth, month); - }, [disablePast, minDate, month, utils]); + }, [disablePast, minDate, month, utils, timezone]); } export function useMeridiemMode( diff --git a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx index ecf93fac68258..660dc91a657eb 100644 --- a/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useDesktopPicker/useDesktopPicker.tsx @@ -36,6 +36,7 @@ export const useDesktopPicker = < sx, format, formatDensity, + timezone, label, inputRef, readOnly, @@ -110,6 +111,7 @@ export const useDesktopPicker = < sx, format, formatDensity, + timezone, label, autoFocus: autoFocus && !props.open, focused: open ? true : undefined, diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts index a14f3d2d107e5..080974de91ee5 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.ts @@ -40,6 +40,7 @@ export const useField = < setTempAndroidValueStr, sectionsValueBoundaries, placeholder, + timezone, } = useFieldState(params); const { @@ -66,7 +67,9 @@ export const useField = < updateSectionValue, sectionsValueBoundaries, setTempAndroidValueStr, + timezone, }); + const inputRef = React.useRef(null); const handleRef = useForkRef(inputRefProp, inputRef); const focusTimeoutRef = React.useRef(undefined); @@ -347,6 +350,7 @@ export const useField = < const newSectionValue = adjustSectionValue( utils, + timezone, activeSection, event.key as AvailableAdjustKeyCode, sectionsValueBoundaries, @@ -403,7 +407,7 @@ export const useField = < }); const validationError = useValidation( - { ...internalProps, value: state.value }, + { ...internalProps, value: state.value, timezone }, validator, valueManager.isSameError, valueManager.defaultErrorState, diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts index e64a6d4a5ea36..c8eb121a0c51b 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.types.ts @@ -4,8 +4,10 @@ import { FieldSection, FieldSelectedSections, MuiPickersAdapter, + TimezoneProps, FieldSectionContentType, FieldValueType, + PickersTimezone, } from '../../../models'; import type { PickerValueManager } from '../usePicker'; import { InferError, Validator } from '../useValidation'; @@ -31,7 +33,8 @@ export interface UseFieldParams< valueType: FieldValueType; } -export interface UseFieldInternalProps { +export interface UseFieldInternalProps + extends TimezoneProps { /** * The selected value. * Used when the component is controlled. @@ -327,8 +330,11 @@ export interface UseFieldState { export type UseFieldValidationProps< TValue, - TInternalProps extends { value?: TValue; defaultValue?: TValue }, -> = Omit & { value: TValue }; + TInternalProps extends { value?: TValue; defaultValue?: TValue; timezone?: PickersTimezone }, +> = Omit & { + value: TValue; + timezone: PickersTimezone; +}; export type AvailableAdjustKeyCode = | 'ArrowUp' diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts index b6317d835842e..e968ff78d85c7 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useField.utils.ts @@ -12,6 +12,7 @@ import { FieldSection, MuiPickersAdapter, FieldSectionContentType, + PickersTimezone, } from '../../../models'; import { PickersLocaleText } from '../../../locales/utils/pickersLocaleTextApi'; import { getMonthsInYear } from '../../utils/date-utils'; @@ -61,10 +62,14 @@ const getDeltaFromKeyCode = (keyCode: Omit(utils: MuiPickersAdapter, format: string) => { +export const getDaysInWeekStr = ( + utils: MuiPickersAdapter, + timezone: PickersTimezone, + format: string, +) => { const elements: TDate[] = []; - const now = utils.date()!; + const now = utils.dateWithTimezone(undefined, timezone)!; const startDate = utils.startOfWeek(now); const endDate = utils.endOfWeek(now); @@ -79,22 +84,23 @@ export const getDaysInWeekStr = (utils: MuiPickersAdapter, format: export const getLetterEditingOptions = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, sectionType: FieldSectionType, format: string, ) => { switch (sectionType) { case 'month': { - return getMonthsInYear(utils, utils.date()!).map((month) => + return getMonthsInYear(utils, utils.dateWithTimezone(undefined, timezone)!).map((month) => utils.formatByString(month, format!), ); } case 'weekDay': { - return getDaysInWeekStr(utils, format); + return getDaysInWeekStr(utils, timezone, format); } case 'meridiem': { - const now = utils.date()!; + const now = utils.dateWithTimezone(undefined, timezone)!; return [utils.startOfDay(now), utils.endOfDay(now)].map((date) => utils.formatByString(date, format), ); @@ -126,6 +132,7 @@ export const cleanLeadingZeros = ( export const cleanDigitSectionValue = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, value: number, sectionBoundaries: FieldSectionValueBoundaries, section: Pick< @@ -169,6 +176,7 @@ export const cleanDigitSectionValue = ( export const adjustSectionValue = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, section: TSection, keyCode: AvailableAdjustKeyCode, sectionsValueBoundaries: FieldSectionsValueBoundaries, @@ -189,7 +197,7 @@ export const adjustSectionValue = ( }); const getCleanValue = (value: number) => - cleanDigitSectionValue(utils, value, sectionBoundaries, section); + cleanDigitSectionValue(utils, timezone, value, sectionBoundaries, section); const step = section.type === 'minutes' && stepsAttribues?.minutesStep ? stepsAttribues.minutesStep : 1; @@ -199,7 +207,7 @@ export const adjustSectionValue = ( if (shouldSetAbsolute) { if (section.type === 'year' && !isEnd && !isStart) { - return utils.formatByString(utils.date()!, section.format); + return utils.formatByString(utils.dateWithTimezone(undefined, timezone)!, section.format); } if (delta > 0 || isStart) { @@ -238,7 +246,7 @@ export const adjustSectionValue = ( }; const adjustLetterSection = () => { - const options = getLetterEditingOptions(utils, section.type, section.format); + const options = getLetterEditingOptions(utils, timezone, section.type, section.format); if (options.length === 0) { return section.value; } @@ -345,6 +353,7 @@ export const addPositionPropertiesToSections = ( const getSectionPlaceholder = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, localeText: PickersLocaleText, sectionConfig: Pick, currentTokenValue: string, @@ -352,7 +361,10 @@ const getSectionPlaceholder = ( switch (sectionConfig.type) { case 'year': { return localeText.fieldYearPlaceholder({ - digitAmount: utils.formatByString(utils.date()!, currentTokenValue).length, + digitAmount: utils.formatByString( + utils.dateWithTimezone(undefined, timezone)!, + currentTokenValue, + ).length, }); } @@ -409,11 +421,15 @@ export const changeSectionValueFormat = ( return utils.formatByString(utils.parse(valueStr, currentFormat)!, newFormat); }; -const isFourDigitYearFormat = (utils: MuiPickersAdapter, format: string) => - utils.formatByString(utils.date()!, format).length === 4; +const isFourDigitYearFormat = ( + utils: MuiPickersAdapter, + timezone: PickersTimezone, + format: string, +) => utils.formatByString(utils.dateWithTimezone(undefined, timezone)!, format).length === 4; export const doesSectionFormatHaveLeadingZeros = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, contentType: FieldSectionContentType, sectionType: FieldSectionType, format: string, @@ -422,40 +438,42 @@ export const doesSectionFormatHaveLeadingZeros = ( return false; } + const now = utils.dateWithTimezone(undefined, timezone)!; + switch (sectionType) { // We can't use `changeSectionValueFormat`, because `utils.parse('1', 'YYYY')` returns `1971` instead of `1`. case 'year': { - if (isFourDigitYearFormat(utils, format)) { - const formatted0001 = utils.formatByString(utils.setYear(utils.date()!, 1), format); + if (isFourDigitYearFormat(utils, timezone, format)) { + const formatted0001 = utils.formatByString(utils.setYear(now, 1), format); return formatted0001 === '0001'; } - const formatted2001 = utils.formatByString(utils.setYear(utils.date()!, 2001), format); + const formatted2001 = utils.formatByString(utils.setYear(now, 2001), format); return formatted2001 === '01'; } case 'month': { - return utils.formatByString(utils.startOfYear(utils.date()!), format).length > 1; + return utils.formatByString(utils.startOfYear(now), format).length > 1; } case 'day': { - return utils.formatByString(utils.startOfMonth(utils.date()!), format).length > 1; + return utils.formatByString(utils.startOfMonth(now), format).length > 1; } case 'weekDay': { - return utils.formatByString(utils.startOfWeek(utils.date()!), format).length > 1; + return utils.formatByString(utils.startOfWeek(now), format).length > 1; } case 'hours': { - return utils.formatByString(utils.setHours(utils.date()!, 1), format).length > 1; + return utils.formatByString(utils.setHours(now, 1), format).length > 1; } case 'minutes': { - return utils.formatByString(utils.setMinutes(utils.date()!, 1), format).length > 1; + return utils.formatByString(utils.setMinutes(now, 1), format).length > 1; } case 'seconds': { - return utils.formatByString(utils.setMinutes(utils.date()!, 1), format).length > 1; + return utils.formatByString(utils.setMinutes(now, 1), format).length > 1; } default: { @@ -480,6 +498,7 @@ const getEscapedPartsFromFormat = (utils: MuiPickersAdapter, forma export const splitFormatIntoSections = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, localeText: PickersLocaleText, format: string, date: TDate | null, @@ -500,6 +519,7 @@ export const splitFormatIntoSections = ( const hasLeadingZerosInFormat = doesSectionFormatHaveLeadingZeros( utils, + timezone, sectionConfig.contentType, sectionConfig.type, token, @@ -537,7 +557,7 @@ export const splitFormatIntoSections = ( format: token, maxLength, value: sectionValue, - placeholder: getSectionPlaceholder(utils, localeText, sectionConfig, token), + placeholder: getSectionPlaceholder(utils, timezone, localeText, sectionConfig, token), hasLeadingZeros: hasLeadingZerosInFormat, hasLeadingZerosInFormat, hasLeadingZerosInInput, @@ -654,7 +674,7 @@ export const getDateFromDateSections = ( const formatWithoutSeparator = sectionFormats.join(' '); const dateWithoutSeparatorStr = sectionValues.join(' '); - return utils.parse(dateWithoutSeparatorStr, formatWithoutSeparator); + return utils.parse(dateWithoutSeparatorStr, formatWithoutSeparator)!; }; export const createDateStrForInputFromSections = (sections: FieldSection[], isRTL: boolean) => { @@ -680,8 +700,9 @@ export const createDateStrForInputFromSections = (sections: FieldSection[], isRT export const getSectionsBoundaries = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, ): FieldSectionsValueBoundaries => { - const today = utils.date()!; + const today = utils.dateWithTimezone(undefined, timezone)!; const endOfYear = utils.endOfYear(today); @@ -701,7 +722,7 @@ export const getSectionsBoundaries = ( return { year: ({ format }) => ({ minimum: 0, - maximum: isFourDigitYearFormat(utils, format) ? 9999 : 99, + maximum: isFourDigitYearFormat(utils, timezone, format) ? 9999 : 99, }), month: () => ({ minimum: 1, @@ -718,7 +739,7 @@ export const getSectionsBoundaries = ( }), weekDay: ({ format, contentType }) => { if (contentType === 'digit') { - const daysInWeek = getDaysInWeekStr(utils, format).map(Number); + const daysInWeek = getDaysInWeekStr(utils, timezone, format).map(Number); return { minimum: Math.min(...daysInWeek), maximum: Math.max(...daysInWeek), @@ -795,6 +816,7 @@ export const validateSections = ( const transferDateSectionValue = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, section: FieldSectionWithoutPosition, dateToTransferFrom: TDate, dateToTransferTo: TDate, @@ -809,7 +831,7 @@ const transferDateSectionValue = ( } case 'weekDay': { - const formattedDaysInWeek = getDaysInWeekStr(utils, section.format); + const formattedDaysInWeek = getDaysInWeekStr(utils, timezone, section.format); const dayInWeekStrOfActiveDate = utils.formatByString(dateToTransferFrom, section.format); const dayInWeekOfActiveDate = formattedDaysInWeek.indexOf(dayInWeekStrOfActiveDate); const dayInWeekOfNewSectionValue = formattedDaysInWeek.indexOf(section.value); @@ -868,6 +890,7 @@ const reliableSectionModificationOrder: Record = { export const mergeDateIntoReferenceDate = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, dateToTransferFrom: TDate, sections: FieldSectionWithoutPosition[], referenceDate: TDate, @@ -880,7 +903,7 @@ export const mergeDateIntoReferenceDate = ( ) .reduce((mergedDate, section) => { if (!shouldLimitToEditedSections || section.modified) { - return transferDateSectionValue(utils, section, dateToTransferFrom, mergedDate); + return transferDateSectionValue(utils, timezone, section, dateToTransferFrom, mergedDate); } return mergedDate; @@ -890,6 +913,7 @@ export const isAndroid = () => navigator.userAgent.toLowerCase().indexOf('androi export const clampDaySectionIfPossible = ( utils: MuiPickersAdapter, + timezone: PickersTimezone, sections: TSection[], sectionsValueBoundaries: FieldSectionsValueBoundaries, ) => { @@ -918,7 +942,7 @@ export const clampDaySectionIfPossible = ( return { ...section, - value: cleanDigitSectionValue(utils, dayBoundaries.minimum, dayBoundaries, section), + value: cleanDigitSectionValue(utils, timezone, dayBoundaries.minimum, dayBoundaries, section), }; }); diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useFieldCharacterEditing.ts b/packages/x-date-pickers/src/internals/hooks/useField/useFieldCharacterEditing.ts index 11d76751c46ff..65de36d9c8c55 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useFieldCharacterEditing.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useFieldCharacterEditing.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import useEventCallback from '@mui/utils/useEventCallback'; -import { FieldSectionType, FieldSection } from '../../../models'; +import { FieldSectionType, FieldSection, PickersTimezone } from '../../../models'; import { useUtils } from '../useUtils'; import { FieldSectionsValueBoundaries } from './useField.types'; import { @@ -29,6 +29,7 @@ interface UseFieldEditingParams { updateSectionValue: (params: UpdateSectionValueParams) => void; sectionsValueBoundaries: FieldSectionsValueBoundaries; setTempAndroidValueStr: (newValue: string | null) => void; + timezone: PickersTimezone; } /** @@ -78,6 +79,7 @@ export const useFieldCharacterEditing = ({ updateSectionValue, sectionsValueBoundaries, setTempAndroidValueStr, + timezone, }: UseFieldEditingParams) => { const utils = useUtils(); @@ -180,7 +182,7 @@ export const useFieldCharacterEditing = ({ formatFallbackValue?: (fallbackValue: string, fallbackOptions: string[]) => string, ) => { const getOptions = (format: string) => - getLetterEditingOptions(utils, activeSection.type, format); + getLetterEditingOptions(utils, timezone, activeSection.type, format); if (activeSection.contentType === 'letter') { return findMatchingOptions( @@ -297,6 +299,7 @@ export const useFieldCharacterEditing = ({ const newSectionValue = cleanDigitSectionValue( utils, + timezone, queryValueNumber, sectionBoundaries, section, @@ -321,6 +324,7 @@ export const useFieldCharacterEditing = ({ if (activeSection.type === 'month') { const hasLeadingZerosInFormat = doesSectionFormatHaveLeadingZeros( utils, + timezone, 'digit', 'month', 'MM', @@ -359,7 +363,7 @@ export const useFieldCharacterEditing = ({ return response; } - const formattedValue = getDaysInWeekStr(utils, activeSection.format)[ + const formattedValue = getDaysInWeekStr(utils, timezone, activeSection.format)[ Number(response.sectionValue) - 1 ]; return { diff --git a/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts b/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts index 570c4b077df6a..8f2e1d0b3cd2b 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts @@ -21,6 +21,7 @@ import { } from './useField.utils'; import { InferError } from '../useValidation'; import { FieldSection, FieldSelectedSections } from '../../../models'; +import { useValueWithTimezone } from '../useValueWithTimezone'; import { GetDefaultReferenceDateProps, getSectionTypeGranularity, @@ -72,19 +73,33 @@ export const useFieldState = < selectedSections: selectedSectionsProp, onSelectedSectionsChange, shouldRespectLeadingZeros = false, + timezone: timezoneProp, }, } = params; - const firstDefaultValue = React.useRef(defaultValue); - const valueFromTheOutside = valueProp ?? firstDefaultValue.current ?? valueManager.emptyValue; + const { + timezone, + value: valueFromTheOutside, + handleValueChange, + } = useValueWithTimezone({ + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager, + }); - const sectionsValueBoundaries = React.useMemo(() => getSectionsBoundaries(utils), [utils]); + const sectionsValueBoundaries = React.useMemo( + () => getSectionsBoundaries(utils, timezone), + [utils, timezone], + ); const getSectionsFromValue = React.useCallback( (value: TValue, fallbackSections: TSection[] | null = null) => fieldValueManager.getSectionsFromValue(utils, value, fallbackSections, isRTL, (date) => splitFormatIntoSections( utils, + timezone, localeText, format, date, @@ -93,7 +108,16 @@ export const useFieldState = < isRTL, ), ), - [fieldValueManager, format, localeText, isRTL, shouldRespectLeadingZeros, utils, formatDensity], + [ + fieldValueManager, + format, + localeText, + isRTL, + shouldRespectLeadingZeros, + utils, + formatDensity, + timezone, + ], ); const placeholder = React.useMemo( @@ -123,6 +147,7 @@ export const useFieldState = < utils, props: internalProps as GetDefaultReferenceDateProps, granularity, + timezone, }); return { @@ -189,13 +214,15 @@ export const useFieldState = < tempValueStrAndroid: null, })); - if (onChange) { - const context: FieldChangeHandlerContext> = { - validationError: validator({ adapter, value, props: { ...internalProps, value } }), - }; + const context: FieldChangeHandlerContext> = { + validationError: validator({ + adapter, + value, + props: { ...internalProps, value, timezone }, + }), + }; - onChange(value, context); - } + handleValueChange(value, context); }; const setSectionValue = (sectionIndex: number, newSectionValue: string) => { @@ -268,6 +295,7 @@ export const useFieldState = < const sections = splitFormatIntoSections( utils, + timezone, localeText, format, date, @@ -275,7 +303,7 @@ export const useFieldState = < shouldRespectLeadingZeros, isRTL, ); - return mergeDateIntoReferenceDate(utils, date, sections, referenceDate, false); + return mergeDateIntoReferenceDate(utils, timezone, date, sections, referenceDate, false); }; const newValue = fieldValueManager.parseValueStr(valueStr, state.referenceValue, parseDateStr); @@ -331,6 +359,7 @@ export const useFieldState = < if (!utils.isValid(newActiveDate)) { const clampedSections = clampDaySectionIfPossible( utils, + timezone, newActiveDateSections, sectionsValueBoundaries, ); @@ -351,6 +380,7 @@ export const useFieldState = < if (newActiveDate != null && utils.isValid(newActiveDate)) { const mergedDate = mergeDateIntoReferenceDate( utils, + timezone, newActiveDate, newActiveDateSections, activeDateManager.referenceDate, @@ -436,5 +466,6 @@ export const useFieldState = < setTempAndroidValueStr, sectionsValueBoundaries, placeholder, + timezone, }; }; diff --git a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx index 54c53a7181af4..d159a86c39b14 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMobilePicker/useMobilePicker.tsx @@ -35,6 +35,7 @@ export const useMobilePicker = < sx, format, formatDensity, + timezone, label, inputRef, readOnly, @@ -84,6 +85,7 @@ export const useMobilePicker = < sx, format, formatDensity, + timezone, label, }, ownerState: props, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts index f6c2c2435432e..0f2022dfbe869 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts @@ -21,6 +21,7 @@ import { PickerValueUpdaterParams, PickerChangeHandlerContext, } from './usePickerValue.types'; +import { useValueWithTimezone } from '../useValueWithTimezone'; /** * Decide if the new value should be published @@ -168,6 +169,7 @@ export const usePickerValue = < closeOnSelect = wrapperVariant === 'desktop', selectedSections: selectedSectionsProp, onSelectedSectionsChange, + timezone: timezoneProp, } = props; const { current: defaultValue } = React.useRef(inDefaultValue); @@ -236,8 +238,16 @@ export const usePickerValue = < }; }); + const { timezone, handleValueChange } = useValueWithTimezone({ + timezone: timezoneProp, + value: inValue, + defaultValue, + onChange, + valueManager, + }); + useValidation( - { ...props, value: dateState.draft }, + { ...props, value: dateState.draft, timezone }, validator, valueManager.isSameError, valueManager.defaultErrorState, @@ -264,21 +274,21 @@ export const usePickerValue = < hasBeenModifiedSinceMount: true, })); - if (shouldPublish && onChange) { + if (shouldPublish) { const validationError = action.name === 'setValueFromField' ? action.context.validationError : validator({ adapter, value: action.value, - props: { ...props, value: action.value }, + props: { ...props, value: action.value, timezone }, }); const context: PickerChangeHandlerContext = { validationError, }; - onChange(action.value, context); + handleValueChange(action.value, context); } if (shouldCommit && onAccept) { @@ -296,6 +306,7 @@ export const usePickerValue = < !valueManager.areValuesEqual(utils, dateState.lastControlledValue, inValue)) ) { const isUpdateComingFromPicker = valueManager.areValuesEqual(utils, dateState.draft, inValue); + setDateState((prev) => ({ ...prev, lastControlledValue: inValue, @@ -344,7 +355,7 @@ export const usePickerValue = < const handleSetToday = useEventCallback(() => { updateDate({ - value: valueManager.getTodayValue(utils, valueType), + value: valueManager.getTodayValue(utils, timezone, valueType), name: 'setValueFromAction', pickerAction: 'today', }); @@ -414,7 +425,7 @@ export const usePickerValue = < const error = validator({ adapter, value: testedValue, - props: { ...props, value: testedValue }, + props: { ...props, value: testedValue, timezone }, }); return !valueManager.hasError(error); 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 8780e9039de8d..30201d35b42d5 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 @@ -6,7 +6,9 @@ import { FieldSection, FieldSelectedSections, FieldValueType, + TimezoneProps, MuiPickersAdapter, + PickersTimezone, } from '../../../models'; import { GetDefaultReferenceDateProps } from '../../utils/getDefaultReferenceDate'; import { PickerShortcutChangeImportance } from '../../../PickersShortcuts'; @@ -33,10 +35,15 @@ export interface PickerValueManager { * Method returning the value to set when clicking the "Today" button * @template TDate, TValue * @param {MuiPickersAdapter} utils The adapter. + * @param {PickersTimezone} timezone The current timezone. * @param {FieldValueType} valueType The type of the value being edited. * @returns {TValue} The value to set when clicking the "Today" button. */ - getTodayValue: (utils: MuiPickersAdapter, valueType: FieldValueType) => TValue; + getTodayValue: ( + utils: MuiPickersAdapter, + timezone: PickersTimezone, + valueType: FieldValueType, + ) => TValue; /** * @template TDate, TValue * Method returning the reference value to use when mounting the component. @@ -45,7 +52,8 @@ export interface PickerValueManager { * @param {TValue} params.value The value provided by the user. * @param {GetDefaultReferenceDateProps} params.props The validation props needed to compute the reference value. * @param {MuiPickersAdapter} params.utils The adapter. - * @param {granularity} params.granularity The granularity of the selection possible on this component. + * @param {number} params.granularity The granularity of the selection possible on this component. + * @param {PickersTimezone} params.timezone The current timezone. * @returns {TValue} The reference value to use for non-provided dates. */ getInitialReferenceValue: (params: { @@ -54,6 +62,7 @@ export interface PickerValueManager { props: GetDefaultReferenceDateProps; utils: MuiPickersAdapter; granularity: number; + timezone: PickersTimezone; }) => TValue; /** * Method parsing the input value to replace all invalid dates by `null`. @@ -104,6 +113,19 @@ export interface PickerValueManager { @returns {string | null} The timezone of the current value. */ getTimezone: (utils: MuiPickersAdapter, value: TValue) => string | null; + /** + * Change the timezone of the dates inside a value. + @template TValue, TDate + @param {MuiPickersAdapter} utils The utils to manipulate the date. + @param {PickersTimezone} timezone The current timezone. + @param {TValue} value The value to convert. + @returns {TValue} The value with the new dates in the new timezone. + */ + setTimezone: ( + utils: MuiPickersAdapter, + timezone: PickersTimezone, + value: TValue, + ) => TValue; } export interface PickerChangeHandlerContext { @@ -254,7 +276,8 @@ export interface UsePickerValueNonStaticProps extends UsePickerValueBaseProps, - UsePickerValueNonStaticProps {} + UsePickerValueNonStaticProps, + TimezoneProps {} export interface UsePickerValueParams< TValue, diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts index d1ca22ca77028..abc2973af228c 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerViews.ts @@ -7,6 +7,7 @@ import { useViews, UseViewsOptions } from '../useViews'; import type { UsePickerValueViewsResponse } from './usePickerValue.types'; import { isTimeView } from '../../utils/time-utils'; import { DateOrTimeViewWithMeridiem } from '../../models'; +import { TimezoneProps } from '../../../models'; interface PickerViewsRendererBaseExternalProps extends Omit, 'openTo' | 'viewRenderers'> {} @@ -51,7 +52,8 @@ export interface UsePickerViewsBaseProps< TView extends DateOrTimeViewWithMeridiem, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, -> extends Omit, 'onChange' | 'onFocusedViewChange' | 'focusedView'> { +> extends Omit, 'onChange' | 'onFocusedViewChange' | 'focusedView'>, + TimezoneProps { /** * If `true`, the picker and text field are disabled. * @default false @@ -143,7 +145,7 @@ export const usePickerViews = < TAdditionalProps >): UsePickerViewsResponse => { const { onChange, open, onSelectedSectionsChange, onClose } = propsFromPickerValue; - const { views, openTo, onViewChange, disableOpenPicker, viewRenderers } = props; + const { views, openTo, onViewChange, disableOpenPicker, viewRenderers, timezone } = props; const { className, sx, ...propsToForwardToView } = props; const { view, setView, defaultView, focusedView, setFocusedView, setValueAndGoToNextView } = @@ -265,6 +267,7 @@ export const usePickerViews = < ...additionalViewProps, ...propsFromPickerValue, views, + timezone, onChange: setValueAndGoToNextView, view: popperView, onViewChange: setView, diff --git a/packages/x-date-pickers/src/internals/hooks/useUtils.ts b/packages/x-date-pickers/src/internals/hooks/useUtils.ts index f52ae18b51369..bef8d49fbd894 100644 --- a/packages/x-date-pickers/src/internals/hooks/useUtils.ts +++ b/packages/x-date-pickers/src/internals/hooks/useUtils.ts @@ -5,6 +5,7 @@ import { } from '../../LocalizationProvider/LocalizationProvider'; import { DEFAULT_LOCALE } from '../../locales/enUS'; import { PickersLocaleText } from '../../locales/utils/pickersLocaleTextApi'; +import { PickersTimezone } from '../../models'; export const useLocalizationContext = () => { const localization = React.useContext(MuiPickersAdapterContext); @@ -50,9 +51,13 @@ export const useDefaultDates = () => useLocalizationContext().defa export const useLocaleText = () => useLocalizationContext().localeText; -export const useNow = (): TDate => { +export const useNow = (timezone: PickersTimezone): TDate => { const utils = useUtils(); - const now = React.useRef(utils.date()); + + const now = React.useRef() as React.MutableRefObject; + if (now.current === undefined) { + now.current = utils.dateWithTimezone(undefined, timezone)!; + } return now.current!; }; diff --git a/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts b/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts new file mode 100644 index 0000000000000..3bac13012c37f --- /dev/null +++ b/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts @@ -0,0 +1,100 @@ +import * as React from 'react'; +import useEventCallback from '@mui/utils/useEventCallback'; +import useControlled from '@mui/utils/useControlled'; +import { useUtils } from './useUtils'; +import type { PickerValueManager } from './usePicker'; +import { PickersTimezone } from '../../models'; + +/** + * Hooks making sure that: + * - The value returned by `onChange` always have the timezone of `props.value` or `props.defaultValue` if defined + * - The value rendered is always the one from `props.timezone` if defined + */ +export const useValueWithTimezone = void>({ + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange, + valueManager, +}: { + timezone: PickersTimezone | undefined; + value: TValue | undefined; + defaultValue: TValue | undefined; + onChange: TChange | undefined; + valueManager: PickerValueManager; +}) => { + const utils = useUtils(); + + const firstDefaultValue = React.useRef(defaultValue); + const inputValue = valueProp ?? firstDefaultValue.current ?? valueManager.emptyValue; + + const inputTimezone = React.useMemo( + () => valueManager.getTimezone(utils, inputValue), + [utils, valueManager, inputValue], + ); + + const setInputTimezone = useEventCallback((newValue: TValue) => { + if (inputTimezone == null) { + return newValue; + } + + return valueManager.setTimezone(utils, inputTimezone, newValue); + }); + + const timezoneToRender = timezoneProp ?? inputTimezone ?? 'default'; + + const valueWithTimezoneToRender = React.useMemo( + () => valueManager.setTimezone(utils, timezoneToRender, inputValue), + [valueManager, utils, timezoneToRender, inputValue], + ); + + const handleValueChange = useEventCallback((newValue: TValue, ...otherParams: any[]) => { + const newValueWithInputTimezone = setInputTimezone(newValue); + onChange?.(newValueWithInputTimezone, ...otherParams); + }) as TChange; + + return { value: valueWithTimezoneToRender, handleValueChange, timezone: timezoneToRender }; +}; + +/** + * Wrapper around `useControlled` and `useValueWithTimezone` + */ +export const useControlledValueWithTimezone = < + TDate, + TValue, + TChange extends (...params: any[]) => void, +>({ + name, + timezone: timezoneProp, + value: valueProp, + defaultValue, + onChange: onChangeProp, + valueManager, +}: { + name: string; + timezone: PickersTimezone | undefined; + value: TValue | undefined; + defaultValue: TValue | undefined; + onChange: TChange | undefined; + valueManager: PickerValueManager; +}) => { + const [valueWithInputTimezone, setValue] = useControlled({ + name, + state: 'value', + controlled: valueProp, + default: defaultValue ?? valueManager.emptyValue, + }); + + const onChange = useEventCallback((newValue: TValue, ...otherParams: any[]) => { + setValue(newValue); + onChangeProp?.(newValue, ...otherParams); + }) as TChange; + + return useValueWithTimezone({ + timezone: timezoneProp, + value: valueWithInputTimezone, + defaultValue: undefined, + onChange, + valueManager, + }); +}; diff --git a/packages/x-date-pickers/src/internals/index.ts b/packages/x-date-pickers/src/internals/index.ts index e14c4907065df..2bdbbe3054e70 100644 --- a/packages/x-date-pickers/src/internals/index.ts +++ b/packages/x-date-pickers/src/internals/index.ts @@ -51,6 +51,7 @@ export { PickersToolbarButton } from './components/PickersToolbarButton'; export { DAY_MARGIN, DIALOG_WIDTH } from './constants/dimensions'; +export { useControlledValueWithTimezone } from './hooks/useValueWithTimezone'; export type { DesktopOnlyPickerProps } from './hooks/useDesktopPicker'; export { useField, 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 d0f6778560740..3acc0e347929d 100644 --- a/packages/x-date-pickers/src/internals/models/props/clock.ts +++ b/packages/x-date-pickers/src/internals/models/props/clock.ts @@ -1,7 +1,7 @@ import { SxProps, Theme } from '@mui/material/styles'; import { BaseTimeValidationProps, TimeValidationProps } from '../validation'; import { PickerSelectionState } from '../../hooks/usePicker/usePickerValue.types'; -import { TimeStepOptions } from '../../../models'; +import { TimeStepOptions, TimezoneProps } from '../../../models'; import type { ExportedDigitalClockProps } from '../../../DigitalClock/DigitalClock.types'; import type { ExportedMultiSectionDigitalClockProps } from '../../../MultiSectionDigitalClock/MultiSectionDigitalClock.types'; import type { ExportedUseViewsOptions } from '../../hooks/useViews'; @@ -9,7 +9,8 @@ import { TimeViewWithMeridiem } from '../common'; export interface ExportedBaseClockProps extends TimeValidationProps, - BaseTimeValidationProps { + BaseTimeValidationProps, + TimezoneProps { /** * 12h/24h view for hour selection clock. * @default `utils.is12HourCycleInCurrentLocale()` diff --git a/packages/x-date-pickers/src/internals/utils/date-utils.test.ts b/packages/x-date-pickers/src/internals/utils/date-utils.test.ts index b1daeb1e86be7..5f9cde15d5d0f 100644 --- a/packages/x-date-pickers/src/internals/utils/date-utils.test.ts +++ b/packages/x-date-pickers/src/internals/utils/date-utils.test.ts @@ -15,6 +15,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: () => true, disableFuture: false, disablePast: false, + timezone: 'default', }); expect(result).to.equal(null); @@ -29,6 +30,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: () => false, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, adapterToUse.date(new Date(2000, 0, 1)))).to.equal(true); @@ -43,6 +45,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: only18th, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, adapterToUse.date(new Date(2018, 7, 18)))).to.equal(true); @@ -57,6 +60,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: only18th, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, adapterToUse.date(new Date(2018, 6, 18)))).to.equal(true); @@ -72,6 +76,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: only18th, disableFuture: false, disablePast: true, + timezone: 'default', })!; expect(adapterToUse.isBefore(result, today)).to.equal(false); @@ -88,6 +93,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: () => false, disableFuture: true, disablePast: true, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, today)).to.equal(true); @@ -103,6 +109,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: (date) => adapterToUse.isSameDay(date, today), disableFuture: true, disablePast: true, + timezone: 'default', }); expect(adapterToUse.isEqual(result, adapterToUse.date())); @@ -117,6 +124,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: only18th, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, adapterToUse.date(new Date(2018, 7, 18)))).to.equal(true); @@ -131,6 +139,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: only18th, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, adapterToUse.date(new Date(2018, 7, 18)))).to.equal(true); @@ -145,6 +154,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: only18th, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, adapterToUse.date(new Date(2018, 6, 18)))).to.equal(true); @@ -159,6 +169,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: only18th, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(adapterToUse.isSameDay(result, adapterToUse.date(new Date(2018, 6, 18)))).to.equal(true); @@ -173,6 +184,7 @@ describe('findClosestEnabledDate', () => { isDateDisabled: () => false, disableFuture: false, disablePast: false, + timezone: 'default', })!; expect(result).to.equal(null); 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 6638e79f13b6e..8d07fff2343ad 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,4 @@ -import { DateView, FieldValueType, MuiPickersAdapter } from '../../models'; +import { DateView, FieldValueType, MuiPickersAdapter, PickersTimezone } from '../../models'; import { DateOrTimeViewWithMeridiem } from '../models'; import { areViewsEqual } from './views'; @@ -10,6 +10,7 @@ interface FindClosestDateParams { minDate: TDate; isDateDisabled: (date: TDate) => boolean; utils: MuiPickersAdapter; + timezone: PickersTimezone; } export const findClosestEnabledDate = ({ @@ -20,8 +21,9 @@ export const findClosestEnabledDate = ({ minDate, isDateDisabled, utils, + timezone, }: FindClosestDateParams) => { - const today = utils.startOfDay(utils.date()!); + const today = utils.startOfDay(utils.dateWithTimezone(undefined, timezone)!); if (disablePast && utils.isBefore(minDate!, today)) { minDate = today; @@ -122,8 +124,14 @@ export const mergeDateAndTime = ( return mergedDate; }; -export const getTodayDate = (utils: MuiPickersAdapter, valueType?: FieldValueType) => - valueType === 'date' ? utils.startOfDay(utils.date()!) : utils.date()!; +export const getTodayDate = ( + utils: MuiPickersAdapter, + timezone: PickersTimezone, + valueType?: FieldValueType, +) => + valueType === 'date' + ? utils.startOfDay(utils.dateWithTimezone(undefined, timezone)!) + : utils.dateWithTimezone(undefined, timezone)!; const dateViews = ['year', 'month', 'day']; export const isDatePickerView = (view: DateOrTimeViewWithMeridiem): view is DateView => diff --git a/packages/x-date-pickers/src/internals/utils/fields.ts b/packages/x-date-pickers/src/internals/utils/fields.ts index d5fb9040ace50..c934eae05e249 100644 --- a/packages/x-date-pickers/src/internals/utils/fields.ts +++ b/packages/x-date-pickers/src/internals/utils/fields.ts @@ -12,6 +12,7 @@ const SHARED_FIELD_INTERNAL_PROP_NAMES = [ 'format', 'formatDensity', 'onChange', + 'timezone', 'readOnly', 'onError', 'shouldRespectLeadingZeros', diff --git a/packages/x-date-pickers/src/internals/utils/getDefaultReferenceDate.ts b/packages/x-date-pickers/src/internals/utils/getDefaultReferenceDate.ts index a4ec06f0648c2..28c10127da31e 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 } from '../../models'; +import { FieldSection, MuiPickersAdapter, PickersTimezone } from '../../models'; export interface GetDefaultReferenceDateProps { maxDate?: TDate; @@ -58,12 +58,14 @@ export const getDefaultReferenceDate = ({ props, utils, granularity, + timezone, }: { props: GetDefaultReferenceDateProps; utils: MuiPickersAdapter; granularity: number; + timezone: PickersTimezone; }) => { - let referenceDate = roundDate(utils, granularity, getTodayDate(utils)); + let referenceDate = roundDate(utils, granularity, getTodayDate(utils, timezone)); if (props.minDate != null && utils.isAfterDay(props.minDate, referenceDate)) { referenceDate = roundDate(utils, granularity, props.minDate); diff --git a/packages/x-date-pickers/src/internals/utils/validation/validateDate.ts b/packages/x-date-pickers/src/internals/utils/validation/validateDate.ts index b7e726010529f..64173f6a59bd8 100644 --- a/packages/x-date-pickers/src/internals/utils/validation/validateDate.ts +++ b/packages/x-date-pickers/src/internals/utils/validation/validateDate.ts @@ -5,14 +5,16 @@ import { MonthValidationProps, YearValidationProps, } from '../../models/validation'; -import { DateValidationError } from '../../../models'; +import { DateValidationError, TimezoneProps } from '../../../models'; import { applyDefaultDate } from '../date-utils'; +import { DefaultizedProps } from '../../models/helpers'; export interface DateComponentValidationProps extends DayValidationProps, MonthValidationProps, YearValidationProps, - Required> {} + Required>, + DefaultizedProps {} export const validateDate: Validator< any | null, @@ -24,7 +26,16 @@ export const validateDate: Validator< return null; } - const now = adapter.utils.date()!; + const { + shouldDisableDate, + shouldDisableMonth, + shouldDisableYear, + disablePast, + disableFuture, + timezone, + } = props; + + const now = adapter.utils.dateWithTimezone(undefined, timezone)!; const minDate = applyDefaultDate(adapter.utils, props.minDate, adapter.defaultDates.minDate); const maxDate = applyDefaultDate(adapter.utils, props.maxDate, adapter.defaultDates.maxDate); @@ -32,19 +43,19 @@ export const validateDate: Validator< case !adapter.utils.isValid(value): return 'invalidDate'; - case Boolean(props.shouldDisableDate && props.shouldDisableDate(value)): + case Boolean(shouldDisableDate && shouldDisableDate(value)): return 'shouldDisableDate'; - case Boolean(props.shouldDisableMonth && props.shouldDisableMonth(value)): + case Boolean(shouldDisableMonth && shouldDisableMonth(value)): return 'shouldDisableMonth'; - case Boolean(props.shouldDisableYear && props.shouldDisableYear(value)): + case Boolean(shouldDisableYear && shouldDisableYear(value)): return 'shouldDisableYear'; - case Boolean(props.disableFuture && adapter.utils.isAfterDay(value, now)): + case Boolean(disableFuture && adapter.utils.isAfterDay(value, now)): return 'disableFuture'; - case Boolean(props.disablePast && adapter.utils.isBeforeDay(value, now)): + case Boolean(disablePast && adapter.utils.isBeforeDay(value, now)): return 'disablePast'; case Boolean(minDate && adapter.utils.isBeforeDay(value, minDate)): diff --git a/packages/x-date-pickers/src/internals/utils/validation/validateTime.ts b/packages/x-date-pickers/src/internals/utils/validation/validateTime.ts index 8475d16c3b2ee..3361f6a53a388 100644 --- a/packages/x-date-pickers/src/internals/utils/validation/validateTime.ts +++ b/packages/x-date-pickers/src/internals/utils/validation/validateTime.ts @@ -1,11 +1,13 @@ import { createIsAfterIgnoreDatePart } from '../time-utils'; import { Validator } from '../../hooks/useValidation'; import { BaseTimeValidationProps, TimeValidationProps } from '../../models/validation'; -import { TimeValidationError } from '../../../models'; +import { TimeValidationError, TimezoneProps } from '../../../models'; +import { DefaultizedProps } from '../../models/helpers'; export interface TimeComponentValidationProps extends Required, - TimeValidationProps {} + TimeValidationProps, + DefaultizedProps {} export const validateTime: Validator< any | null, @@ -13,6 +15,10 @@ export const validateTime: Validator< TimeValidationError, TimeComponentValidationProps > = ({ adapter, value, props }): TimeValidationError => { + if (value === null) { + return null; + } + const { minTime, maxTime, @@ -22,19 +28,15 @@ export const validateTime: Validator< disableIgnoringDatePartForTimeValidation = false, disablePast, disableFuture, + timezone, } = props; - const now = adapter.utils.date()!; - const date = adapter.utils.date(value); + const now = adapter.utils.dateWithTimezone(undefined, timezone)!; const isAfter = createIsAfterIgnoreDatePart( disableIgnoringDatePartForTimeValidation, adapter.utils, ); - if (value === null) { - return null; - } - switch (true) { case !adapter.utils.isValid(value): return 'invalidDate'; @@ -45,10 +47,10 @@ export const validateTime: Validator< case Boolean(maxTime && isAfter(value, maxTime)): return 'maxTime'; - case Boolean(disableFuture && adapter.utils.isAfter(date, now)): + case Boolean(disableFuture && adapter.utils.isAfter(value, now)): return 'disableFuture'; - case Boolean(disablePast && adapter.utils.isBefore(date, now)): + case Boolean(disablePast && adapter.utils.isBefore(value, now)): return 'disablePast'; case Boolean(shouldDisableTime && shouldDisableTime(value, 'hours')): diff --git a/packages/x-date-pickers/src/internals/utils/valueManagers.ts b/packages/x-date-pickers/src/internals/utils/valueManagers.ts index 3f8b0f5145c14..2772270304f5d 100644 --- a/packages/x-date-pickers/src/internals/utils/valueManagers.ts +++ b/packages/x-date-pickers/src/internals/utils/valueManagers.ts @@ -39,6 +39,8 @@ export const singleItemValueManager: SingleItemPickerValueManager = { hasError: (error) => error != null, defaultErrorState: null, getTimezone: (utils, value) => (value == null ? null : utils.getTimezone(value)), + setTimezone: (utils, timezone, value) => + value == null ? null : utils.setTimezone(value, timezone), }; export const singleItemFieldValueManager: FieldValueManager = { diff --git a/packages/x-date-pickers/src/models/adapters.ts b/packages/x-date-pickers/src/models/adapters.ts index cc5776da25570..b7ea6f443529e 100644 --- a/packages/x-date-pickers/src/models/adapters.ts +++ b/packages/x-date-pickers/src/models/adapters.ts @@ -233,7 +233,7 @@ export interface MuiPickersAdapter { * If a `value` parameter is provided, pass it to the date library to try to parse it. * @template TDate * @param {string | null | undefined} value The optional value to parse. - * @param {string} timezone The timezone of the date. + * @param {PickersTimezone} timezone The timezone of the date. * @returns {TDate | null} The parsed date. */ dateWithTimezone(value: string | null | undefined, timezone: PickersTimezone): TDate | null; @@ -247,7 +247,7 @@ export interface MuiPickersAdapter { * Convert a date to another timezone. * @template TDate * @param {TDate} value The date to convert. - * @param {string} timezone The timezone to convert the date to. + * @param {PickersTimezone} timezone The timezone to convert the date to. * @returns {TDate} The converted date. */ setTimezone(value: TDate, timezone: PickersTimezone): TDate; diff --git a/packages/x-date-pickers/src/models/timezone.ts b/packages/x-date-pickers/src/models/timezone.ts index 7bbd896dd0383..736bd6bc79852 100644 --- a/packages/x-date-pickers/src/models/timezone.ts +++ b/packages/x-date-pickers/src/models/timezone.ts @@ -1 +1,12 @@ export type PickersTimezone = 'default' | 'system' | 'UTC' | string; + +export interface TimezoneProps { + /** + * Choose which timezone to use for the value. + * Example: "default", "system", "UTC", "America/New_York". + * If you pass values from other timezones to some props, they will be converted to this timezone before being used. + * @see See the {@link https://mui.com/x/react-date-pickers/timezone/ timezones documention} for more details. + * @default The timezone of the `value` or `defaultValue` prop is defined, 'default' otherwise. + */ + timezone?: PickersTimezone; +} diff --git a/packages/x-date-pickers/src/tests/describeAdapters/describeAdapters.ts b/packages/x-date-pickers/src/tests/describeAdapters/describeAdapters.ts index 39954a7189c5b..2814595116510 100644 --- a/packages/x-date-pickers/src/tests/describeAdapters/describeAdapters.ts +++ b/packages/x-date-pickers/src/tests/describeAdapters/describeAdapters.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import moment from 'moment'; +import momentTZ from 'moment-timezone'; import createDescribe from '@mui/monorepo/test/utils/createDescribe'; import { AdapterName, @@ -30,6 +31,7 @@ function innerDescribeAdapters

( adapterName, clock: 'fake', clockConfig: new Date(2022, 5, 15), + instance: adapterName === 'moment' ? momentTZ : undefined, }); const fieldInteractions = buildFieldInteractions

({ diff --git a/packages/x-date-pickers/src/tests/describeGregorianAdapter/describeGregorianAdapter.types.ts b/packages/x-date-pickers/src/tests/describeGregorianAdapter/describeGregorianAdapter.types.ts index 57288383040df..052f0a1969422 100644 --- a/packages/x-date-pickers/src/tests/describeGregorianAdapter/describeGregorianAdapter.types.ts +++ b/packages/x-date-pickers/src/tests/describeGregorianAdapter/describeGregorianAdapter.types.ts @@ -3,6 +3,7 @@ import { MuiPickersAdapter, PickersTimezone } from '@mui/x-date-pickers/models'; export interface DescribeGregorianAdapterParams { prepareAdapter?: (adapter: MuiPickersAdapter) => void; formatDateTime: string; + getLocaleFromDate?: (value: TDate) => string; dateLibInstanceWithTimezoneSupport?: any; setDefaultTimezone: (timezone: PickersTimezone | undefined) => void; frenchLocale: TLocale; diff --git a/packages/x-date-pickers/src/tests/describeGregorianAdapter/testCalculations.ts b/packages/x-date-pickers/src/tests/describeGregorianAdapter/testCalculations.ts index 9f7618946a5d2..59d0de42e3649 100644 --- a/packages/x-date-pickers/src/tests/describeGregorianAdapter/testCalculations.ts +++ b/packages/x-date-pickers/src/tests/describeGregorianAdapter/testCalculations.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { PickersTimezone } from '@mui/x-date-pickers/models'; +import { getDateOffset } from 'test/utils/pickers-utils'; import { DescribeGregorianAdapterTestSuite } from './describeGregorianAdapter.types'; import { TEST_DATE_ISO_STRING, TEST_DATE_LOCALE_STRING } from './describeGregorianAdapter.utils'; @@ -8,6 +9,7 @@ export const testCalculations: DescribeGregorianAdapterTestSuite = ({ adapterTZ, adapterFr, setDefaultTimezone, + getLocaleFromDate, }) => { const testDateIso = adapter.date(TEST_DATE_ISO_STRING)!; const testDateLocale = adapter.date(TEST_DATE_LOCALE_STRING)!; @@ -153,27 +155,30 @@ export const testCalculations: DescribeGregorianAdapterTestSuite = ({ setDefaultTimezone(undefined); }); - describe('Method: setTimezone', () => { - it('should support "default"', () => { - if (adapter.isTimezoneCompatible) { - const test = (timezone: PickersTimezone) => { - setDefaultTimezone(timezone); - const dateWithLocaleTimezone = adapter.dateWithTimezone(undefined, 'system')!; - const dateWithDefaultTimezone = adapter.setTimezone(dateWithLocaleTimezone, 'default'); + it('Method: setTimezone', () => { + if (adapter.isTimezoneCompatible) { + const test = (timezone: PickersTimezone) => { + setDefaultTimezone(timezone); + const dateWithLocaleTimezone = adapter.dateWithTimezone(TEST_DATE_ISO_STRING, 'system')!; + const dateWithDefaultTimezone = adapter.setTimezone(dateWithLocaleTimezone, 'default'); - expect(adapter.getTimezone(dateWithDefaultTimezone)).to.equal(timezone); - }; + expect(adapter.getTimezone(dateWithDefaultTimezone)).to.equal(timezone); + }; - test('America/New_York'); - test('Europe/Paris'); + test('America/New_York'); + test('Europe/Paris'); - // Reset to the default timezone - setDefaultTimezone(undefined); - } else { - const localeDate = adapter.dateWithTimezone(undefined, 'system')!; - expect(adapter.setTimezone(localeDate, 'default')).to.equal(localeDate); - } - }); + // Reset to the default timezone + setDefaultTimezone(undefined); + } else { + const systemDate = adapter.dateWithTimezone(TEST_DATE_ISO_STRING, 'system')!; + expect(adapter.setTimezone(systemDate, 'default')).toEqualDateTime(systemDate); + expect(adapter.setTimezone(systemDate, 'system')).toEqualDateTime(systemDate); + + const defaultDate = adapter.dateWithTimezone(TEST_DATE_ISO_STRING, 'default')!; + expect(adapter.setTimezone(systemDate, 'default')).toEqualDateTime(defaultDate); + expect(adapter.setTimezone(systemDate, 'system')).toEqualDateTime(defaultDate); + } }); it('Method: toJsDate', () => { @@ -191,9 +196,6 @@ export const testCalculations: DescribeGregorianAdapterTestSuite = ({ if (adapter.lib === 'date-fns') { // date-fns never suppress useless milliseconds in the end expect(outputtedISO).to.equal(TEST_DATE_ISO_STRING.replace('.000Z', 'Z')); - } else if (adapter.lib === 'luxon') { - // luxon does not shorthand +00:00 to Z, which is also valid ISO string - expect(outputtedISO).to.equal(TEST_DATE_ISO_STRING.replace('Z', '+00:00')); } else { expect(outputtedISO).to.equal(TEST_DATE_ISO_STRING); } @@ -813,6 +815,10 @@ export const testCalculations: DescribeGregorianAdapterTestSuite = ({ expect(adapter.getSeconds(testDateIso)).to.equal(0); }); + it('Method: getMilliseconds', () => { + expect(adapter.getMilliseconds(testDateIso)).to.equal(0); + }); + it('Method: setYear', () => { expect(adapter.setYear(testDateIso, 2011)).toEqualDateTime('2011-10-30T11:44:00.000Z'); }); @@ -837,6 +843,10 @@ export const testCalculations: DescribeGregorianAdapterTestSuite = ({ expect(adapter.setSeconds(testDateIso, 11)).toEqualDateTime('2018-10-30T11:44:11.000Z'); }); + it('Method: setMilliseconds', () => { + expect(adapter.setMilliseconds(testDateIso, 11)).toEqualDateTime('2018-10-30T11:44:00.011Z'); + }); + it('Method: getDaysInMonth', () => { expect(adapter.getDaysInMonth(testDateIso)).to.equal(31); expect(adapter.getDaysInMonth(testDateLocale)).to.equal(31); @@ -880,12 +890,55 @@ export const testCalculations: DescribeGregorianAdapterTestSuite = ({ }); }); - it('Method: getWeekArray', () => { - const weekArray = adapter.getWeekArray(testDateIso); + describe('Method: getWeekArray', () => { + it('should work without timezones', () => { + const weekArray = adapter.getWeekArray(testDateIso); + let expectedDate = adapter.startOfWeek(adapter.startOfMonth(testDateIso)); + + expect(weekArray).to.have.length(5); + weekArray.forEach((week) => { + expect(week).to.have.length(7); + week.forEach((day) => { + expect(day).toEqualDateTime(expectedDate); + expectedDate = adapter.addDays(expectedDate, 1); + }); + }); + }); + + it('should respect the DST', function test() { + if (!adapterTZ.isTimezoneCompatible) { + this.skip(); + } + + const referenceDate = adapterTZ.dateWithTimezone('2022-03-17', 'Europe/Paris')!; + const weekArray = adapterTZ.getWeekArray(referenceDate); + let expectedDate = adapter.startOfWeek(adapter.startOfMonth(referenceDate)); + const lastNonDSTDay = adapterTZ.dateWithTimezone('2022-03-27', 'Europe/Paris')!; + + expect(weekArray).to.have.length(5); + weekArray.forEach((week) => { + expect(week).to.have.length(7); + week.forEach((day) => { + expect(adapterTZ.startOfDay(day)).toEqualDateTime(adapterTZ.startOfDay(expectedDate)); + expectedDate = adapterTZ.addDays(expectedDate, 1); + + expect(getDateOffset(adapterTZ, day)).to.equal( + adapterTZ.isAfter(day, lastNonDSTDay) ? 120 : 60, + ); + }); + }); + }); + + it('should respect the locale of the adapter, not the locale of the date', function test() { + const dateFr = adapterFr.dateWithTimezone('2022-03-17', 'default')!; + const weekArray = adapter.getWeekArray(dateFr); + + if (getLocaleFromDate) { + expect(getLocaleFromDate(weekArray[0][0])).to.match(/en/); + } - expect(weekArray).to.have.length(5); - weekArray.forEach((week) => { - expect(week).to.have.length(7); + // Week should start on Monday (28th of March) for adapters supporting locale-based week start. + expect(adapter.getDate(weekArray[0][0])).to.equal(adapter.lib === 'luxon' ? 28 : 27); }); }); diff --git a/packages/x-date-pickers/src/tests/describeJalaliAdapter/testCalculations.ts b/packages/x-date-pickers/src/tests/describeJalaliAdapter/testCalculations.ts index 0f319e09b9dc0..6e35e987851da 100644 --- a/packages/x-date-pickers/src/tests/describeJalaliAdapter/testCalculations.ts +++ b/packages/x-date-pickers/src/tests/describeJalaliAdapter/testCalculations.ts @@ -269,6 +269,18 @@ export const testCalculations: DescribeJalaliAdapterTestSuite = ({ adapter }) => expect(adapter.getDate(testDateIso)).to.equal(8); }); + it('Method: getMinutes', () => { + expect(adapter.getMinutes(testDateIso)).to.equal(44); + }); + + it('Method: getSeconds', () => { + expect(adapter.getSeconds(testDateIso)).to.equal(0); + }); + + it('Method: getMilliseconds', () => { + expect(adapter.getMilliseconds(testDateIso)).to.equal(0); + }); + it('Method: setYear', () => { expect(adapter.setYear(testDateIso, 1398)).toEqualDateTime('2019-10-30T11:44:00.000Z'); }); @@ -281,6 +293,22 @@ export const testCalculations: DescribeJalaliAdapterTestSuite = ({ adapter }) => expect(adapter.setDate(testDateIso, 9)).toEqualDateTime('2018-10-31T11:44:00.000Z'); }); + it('Method: setHours', () => { + expect(adapter.setHours(testDateIso, 0)).toEqualDateTime('2018-10-30T00:44:00.000Z'); + }); + + it('Method: setMinutes', () => { + expect(adapter.setMinutes(testDateIso, 12)).toEqualDateTime('2018-10-30T11:12:00.000Z'); + }); + + it('Method: setSeconds', () => { + expect(adapter.setSeconds(testDateIso, 11)).toEqualDateTime('2018-10-30T11:44:11.000Z'); + }); + + it('Method: setMilliseconds', () => { + expect(adapter.setMilliseconds(testDateIso, 11)).toEqualDateTime('2018-10-30T11:44:00.011Z'); + }); + it('Method: getNextMonth', () => { expect(adapter.getNextMonth(testDateIso)).toEqualDateTime('2018-11-29T11:44:00.000Z'); }); diff --git a/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx b/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx index 539511d8c35ec..7d9a171827d74 100644 --- a/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx +++ b/packages/x-date-pickers/src/timeViewRenderers/timeViewRenderers.tsx @@ -50,6 +50,7 @@ export const renderTimeViewClock = ({ autoFocus, showViewSwitcher, disableIgnoringDatePartForTimeValidation, + timezone, }: TimeViewRendererProps>) => ( view={view} @@ -81,6 +82,7 @@ export const renderTimeViewClock = ({ autoFocus={autoFocus} showViewSwitcher={showViewSwitcher} disableIgnoringDatePartForTimeValidation={disableIgnoringDatePartForTimeValidation} + timezone={timezone} /> ); @@ -114,6 +116,7 @@ export const renderDigitalClockTimeView = ({ disableIgnoringDatePartForTimeValidation, timeSteps, skipDisabled, + timezone, }: TimeViewRendererProps< Extract, Omit, 'timeStep'> & Pick, 'timeSteps'> @@ -148,6 +151,7 @@ export const renderDigitalClockTimeView = ({ disableIgnoringDatePartForTimeValidation={disableIgnoringDatePartForTimeValidation} timeStep={timeSteps?.minutes} skipDisabled={skipDisabled} + timezone={timezone} /> ); @@ -181,6 +185,7 @@ export const renderMultiSectionDigitalClockTimeView = ({ disableIgnoringDatePartForTimeValidation, timeSteps, skipDisabled, + timezone, }: TimeViewRendererProps>) => ( view={view} @@ -212,5 +217,6 @@ export const renderMultiSectionDigitalClockTimeView = ({ disableIgnoringDatePartForTimeValidation={disableIgnoringDatePartForTimeValidation} timeSteps={timeSteps} skipDisabled={skipDisabled} + timezone={timezone} /> ); diff --git a/scripts/x-date-pickers-pro.exports.json b/scripts/x-date-pickers-pro.exports.json index a6406cd4c0db8..4f5fcb6d8f0fb 100644 --- a/scripts/x-date-pickers-pro.exports.json +++ b/scripts/x-date-pickers-pro.exports.json @@ -310,6 +310,7 @@ { "name": "TimeValidationError", "kind": "TypeAlias" }, { "name": "TimeView", "kind": "TypeAlias" }, { "name": "TimeViewRendererProps", "kind": "TypeAlias" }, + { "name": "TimezoneProps", "kind": "Interface" }, { "name": "trTR", "kind": "Variable" }, { "name": "ukUA", "kind": "Variable" }, { "name": "unstable_useDateField", "kind": "Variable" }, diff --git a/scripts/x-date-pickers.exports.json b/scripts/x-date-pickers.exports.json index e3a4e1024bb93..add4196e8815a 100644 --- a/scripts/x-date-pickers.exports.json +++ b/scripts/x-date-pickers.exports.json @@ -250,6 +250,7 @@ { "name": "TimeValidationError", "kind": "TypeAlias" }, { "name": "TimeView", "kind": "TypeAlias" }, { "name": "TimeViewRendererProps", "kind": "TypeAlias" }, + { "name": "TimezoneProps", "kind": "Interface" }, { "name": "trTR", "kind": "Variable" }, { "name": "ukUA", "kind": "Variable" }, { "name": "unstable_useDateField", "kind": "Variable" }, diff --git a/test/utils/pickers-utils.tsx b/test/utils/pickers-utils.tsx index 3ccf62ffae11c..cef275cf578f3 100644 --- a/test/utils/pickers-utils.tsx +++ b/test/utils/pickers-utils.tsx @@ -31,6 +31,7 @@ import { import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { CLOCK_WIDTH } from '@mui/x-date-pickers/TimeClock/shared'; import { PickerComponentFamily } from '@mui/x-date-pickers/tests/describe.types'; +import { clockPointerClasses } from '@mui/x-date-pickers'; export type AdapterName = | 'date-fns' @@ -659,3 +660,26 @@ export const getExpectedOnChangeCount = ( } return getChangeCountForComponentFamily(componentFamily); }; + +export const getTimeClockValue = () => { + const clockPointer = document.querySelector(`.${clockPointerClasses.root}`); + const transform = clockPointer?.style?.transform ?? ''; + const isMinutesView = screen.getByRole('listbox').getAttribute('aria-label')?.includes('minutes'); + + const rotation = Number(/rotateZ\(([0-9]+)deg\)/.exec(transform)?.[1] ?? '0'); + + if (isMinutesView) { + return rotation / 6; + } + + return rotation / 30; +}; + +export const getDateOffset = ( + adapter: MuiPickersAdapter, + date: TDate, +) => { + const utcHour = adapter.getHours(adapter.setTimezone(adapter.startOfDay(date), 'UTC')); + const cleanUtcHour = utcHour > 12 ? 24 - utcHour : -utcHour; + return cleanUtcHour * 60; +}; diff --git a/yarn.lock b/yarn.lock index 1a5ef5df9ef96..3a989fe263a09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10091,6 +10091,13 @@ moment-timezone@^0.5.21: dependencies: moment ">= 2.9.0" +moment-timezone@^0.5.41: + version "0.5.41" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.41.tgz#a7ad3285fd24aaf5f93b8119a9d749c8039c64c5" + integrity sha512-e0jGNZDOHfBXJGz8vR/sIMXvBIGJJcqFjmlg9lmE+5KX1U7/RZNMswfD8nKnNCnQdKTIj50IaRKwl1fvMLyyRg== + dependencies: + moment "^2.29.4" + "moment@>= 2.9.0", moment@>=2.14.0, moment@^2.22.2, moment@^2.24.0, moment@^2.29.4: version "2.29.4" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"