diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx index f4c4ba2bf64d1..be7fe1d16317f 100644 --- a/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/DateRangeCalendar.tsx @@ -208,13 +208,14 @@ const DateRangeCalendar = React.forwardRef(function DateRangeCalendar< } = props; const { value, handleValueChange, timezone } = useControlledValueWithTimezone< - TDate, DateRange, + TDate, NonNullable >({ name: 'DateRangeCalendar', timezone: timezoneProp, value: valueProp, + referenceDate, defaultValue, onChange, valueManager: rangeValueManager, diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts index f89396ebc68bf..5e2cf61c67ee3 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateRangeField.ts @@ -47,6 +47,7 @@ export const useMultiInputDateRangeField = < const { value: valueProp, defaultValue, + referenceDate, format, formatDensity, shouldRespectLeadingZeros, @@ -65,6 +66,7 @@ export const useMultiInputDateRangeField = < timezone: timezoneProp, value: valueProp, defaultValue, + referenceDate, onChange, valueManager: rangeValueManager, }); diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts index 60efbe608a10a..1d7e44ce0b655 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputDateTimeRangeField.ts @@ -47,6 +47,7 @@ export const useMultiInputDateTimeRangeField = < const { value: valueProp, defaultValue, + referenceDate, format, formatDensity, shouldRespectLeadingZeros, @@ -65,6 +66,7 @@ export const useMultiInputDateTimeRangeField = < timezone: timezoneProp, value: valueProp, defaultValue, + referenceDate, onChange, valueManager: rangeValueManager, }); diff --git a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts index 86db843894f5a..a5947805371f1 100644 --- a/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts +++ b/packages/x-date-pickers-pro/src/internals/hooks/useMultiInputRangeField/useMultiInputTimeRangeField.ts @@ -47,6 +47,7 @@ export const useMultiInputTimeRangeField = < const { value: valueProp, defaultValue, + referenceDate, format, formatDensity, shouldRespectLeadingZeros, @@ -67,6 +68,7 @@ export const useMultiInputTimeRangeField = < defaultValue, onChange, valueManager: rangeValueManager, + referenceDate, }); const { validationError, getValidationErrorForNewValue } = useValidation({ diff --git a/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx b/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx index 77af1660e28fa..a009d7afa8160 100644 --- a/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx +++ b/packages/x-date-pickers/src/DateCalendar/DateCalendar.tsx @@ -154,6 +154,7 @@ export const DateCalendar = React.forwardRef(function DateCalendar void, + referenceDate: referenceDateProp, + onChange, valueManager: singleItemValueManager, }); diff --git a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx index f9db7217d2387..228a0d365317f 100644 --- a/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx +++ b/packages/x-date-pickers/src/MultiSectionDigitalClock/MultiSectionDigitalClock.tsx @@ -112,6 +112,7 @@ export const MultiSectionDigitalClock = React.forwardRef(function MultiSectionDi timezone: timezoneProp, value: valueProp, defaultValue, + referenceDate: referenceDateProp, onChange, valueManager: singleItemValueManager, }); diff --git a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx index df5213454067a..e7740592ce1db 100644 --- a/packages/x-date-pickers/src/TimeClock/TimeClock.tsx +++ b/packages/x-date-pickers/src/TimeClock/TimeClock.tsx @@ -114,6 +114,7 @@ export const TimeClock = React.forwardRef(function TimeClock void, + referenceDate: referenceDateProp, + onChange, valueManager: singleItemValueManager, }); 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 0e87524ab21e6..874f1685761b5 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/useFieldState.ts @@ -120,6 +120,7 @@ export const useFieldState = < timezone: timezoneProp, value: valueProp, defaultValue, + referenceDate: referenceDateProp, onChange, valueManager, }); diff --git a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts index 5bc23631a90dd..fabcdead5264d 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePicker.types.ts @@ -35,7 +35,7 @@ export interface UsePickerProps< TError, TExternalProps extends UsePickerViewsProps, TAdditionalProps extends {}, -> extends UsePickerValueProps, +> extends UsePickerValueProps, UsePickerViewsProps, UsePickerLayoutProps {} 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 5c22920d367d2..b904e7d3db602 100644 --- a/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts +++ b/packages/x-date-pickers/src/internals/hooks/usePicker/usePickerValue.ts @@ -154,7 +154,7 @@ export const usePickerValue = < TValue, TDate extends PickerValidDate, TSection extends FieldSection, - TExternalProps extends UsePickerValueProps, + TExternalProps extends UsePickerValueProps, >({ props, valueManager, @@ -175,6 +175,7 @@ export const usePickerValue = < defaultValue: inDefaultValue, closeOnSelect = wrapperVariant === 'desktop', timezone: timezoneProp, + referenceDate, } = props; const { current: defaultValue } = React.useRef(inDefaultValue); @@ -225,6 +226,7 @@ export const usePickerValue = < timezone: timezoneProp, value: inValueWithoutRenderTimezone, defaultValue, + referenceDate, onChange, valueManager, }); 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 c01be0eaefd3e..8199914e8f1a5 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 @@ -267,15 +267,18 @@ export interface UsePickerValueNonStaticProps { /** * Props used to handle the value of the pickers. */ -export interface UsePickerValueProps +export interface UsePickerValueProps extends UsePickerValueBaseProps, UsePickerValueNonStaticProps, - TimezoneProps {} + TimezoneProps { + // We don't add JSDoc here because we want the `referenceDate` JSDoc to be the one from the view which has more context. + referenceDate?: TDate; +} export interface UsePickerValueParams< TValue, TDate extends PickerValidDate, - TExternalProps extends UsePickerValueProps, + TExternalProps extends UsePickerValueProps, > { props: TExternalProps; valueManager: PickerValueManager>; diff --git a/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.test.tsx b/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.test.tsx new file mode 100644 index 0000000000000..b74ec03b2deea --- /dev/null +++ b/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.test.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { screen } from '@mui/internal-test-utils'; +import { PickersTimezone, PickerValidDate } from '@mui/x-date-pickers/models'; +import { createPickerRenderer } from 'test/utils/pickers'; +import { useValueWithTimezone } from './useValueWithTimezone'; +import { singleItemValueManager } from '../utils/valueManagers'; + +describe('useValueWithTimezone', () => { + const { render, adapter } = createPickerRenderer({ + clock: 'fake', + adapterName: 'luxon', + }); + + function runTest(params: { + timezone: PickersTimezone | undefined; + value: PickerValidDate | null | undefined; + defaultValue: PickerValidDate | null | undefined; + referenceDate: PickerValidDate | undefined; + expectedTimezone: PickersTimezone; + }) { + const { expectedTimezone, ...other } = params; + + function TestComponent(props: typeof other) { + const { timezone } = useValueWithTimezone({ + ...props, + valueManager: singleItemValueManager, + onChange: () => {}, + }); + + return
{timezone}
; + } + + render(); + + expect(screen.getByTestId('result').textContent).to.equal(expectedTimezone); + } + + it('should use the timezone parameter when provided', () => { + runTest({ + timezone: 'America/New_York', + value: undefined, + defaultValue: undefined, + referenceDate: undefined, + expectedTimezone: 'America/New_York', + }); + }); + + it('should use the timezone parameter over the value parameter when both are provided', () => { + runTest({ + timezone: 'America/New_York', + value: adapter.date(undefined, 'Europe/Paris'), + defaultValue: undefined, + referenceDate: undefined, + expectedTimezone: 'America/New_York', + }); + }); + + it('should use the value parameter when provided', () => { + runTest({ + timezone: undefined, + value: adapter.date(undefined, 'America/New_York'), + defaultValue: undefined, + referenceDate: undefined, + expectedTimezone: 'America/New_York', + }); + }); + + it('should use the value parameter over the defaultValue parameter when both are provided', () => { + runTest({ + timezone: undefined, + value: adapter.date(undefined, 'America/New_York'), + defaultValue: adapter.date(undefined, 'Europe/Paris'), + referenceDate: undefined, + expectedTimezone: 'America/New_York', + }); + }); + + it('should use the defaultValue parameter when provided', () => { + runTest({ + timezone: undefined, + value: undefined, + defaultValue: adapter.date(undefined, 'America/New_York'), + referenceDate: undefined, + expectedTimezone: 'America/New_York', + }); + }); + + it('should use the referenceDate parameter when provided', () => { + runTest({ + timezone: undefined, + value: undefined, + defaultValue: undefined, + referenceDate: adapter.date(undefined, 'America/New_York'), + expectedTimezone: 'America/New_York', + }); + }); + + it('should use the "default" timezone is there is no way to deduce the user timezone', () => { + runTest({ + timezone: undefined, + value: undefined, + defaultValue: undefined, + referenceDate: undefined, + expectedTimezone: 'default', + }); + }); +}); diff --git a/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts b/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts index dd892c5d1c9da..7d91a6ac5cfb9 100644 --- a/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts +++ b/packages/x-date-pickers/src/internals/hooks/useValueWithTimezone.ts @@ -11,22 +11,17 @@ import { PickersTimezone, PickerValidDate } from '../../models'; * - The value rendered is always the one from `props.timezone` if defined */ export const useValueWithTimezone = < - TDate extends PickerValidDate, TValue, + TDate extends PickerValidDate, TChange extends (...params: any[]) => void, >({ timezone: timezoneProp, value: valueProp, defaultValue, + referenceDate, onChange, valueManager, -}: { - timezone: PickersTimezone | undefined; - value: TValue | undefined; - defaultValue: TValue | undefined; - onChange: TChange | undefined; - valueManager: PickerValueManager; -}) => { +}: UseValueWithTimezoneParameters) => { const utils = useUtils(); const firstDefaultValue = React.useRef(defaultValue); @@ -45,7 +40,16 @@ export const useValueWithTimezone = < return valueManager.setTimezone(utils, inputTimezone, newValue); }); - const timezoneToRender = timezoneProp ?? inputTimezone ?? 'default'; + let timezoneToRender: PickersTimezone; + if (timezoneProp) { + timezoneToRender = timezoneProp; + } else if (inputTimezone) { + timezoneToRender = inputTimezone; + } else if (referenceDate) { + timezoneToRender = utils.getTimezone(referenceDate); + } else { + timezoneToRender = 'default'; + } const valueWithTimezoneToRender = React.useMemo( () => valueManager.setTimezone(utils, timezoneToRender, inputValue), @@ -64,24 +68,18 @@ export const useValueWithTimezone = < * Wrapper around `useControlled` and `useValueWithTimezone` */ export const useControlledValueWithTimezone = < - TDate extends PickerValidDate, TValue, + TDate extends PickerValidDate, TChange extends (...params: any[]) => void, >({ name, timezone: timezoneProp, value: valueProp, defaultValue, + referenceDate, onChange: onChangeProp, valueManager, -}: { - name: string; - timezone: PickersTimezone | undefined; - value: TValue | undefined; - defaultValue: TValue | undefined; - onChange: TChange | undefined; - valueManager: PickerValueManager; -}) => { +}: UseControlledValueWithTimezoneParameters) => { const [valueWithInputTimezone, setValue] = useControlled({ name, state: 'value', @@ -98,7 +96,34 @@ export const useControlledValueWithTimezone = < timezone: timezoneProp, value: valueWithInputTimezone, defaultValue: undefined, + referenceDate, onChange, valueManager, }); }; + +interface UseValueWithTimezoneParameters< + TValue, + TDate extends PickerValidDate, + TChange extends (...params: any[]) => void, +> { + timezone: PickersTimezone | undefined; + value: TValue | undefined; + defaultValue: TValue | undefined; + /** + * The reference date as passed to `props.referenceDate`. + * It does not need to have its default value. + * This is only used to determine the timezone to use when `props.value` and `props.defaultValue` are not defined. + */ + referenceDate: TDate | undefined; + onChange: TChange | undefined; + valueManager: PickerValueManager; +} + +interface UseControlledValueWithTimezoneParameters< + TValue, + TDate extends PickerValidDate, + TChange extends (...params: any[]) => void, +> extends UseValueWithTimezoneParameters { + name: string; +}