From 7d2c714379c5a2ecea3230c17a8fa7efb545afb8 Mon Sep 17 00:00:00 2001 From: Lukas Tyla Date: Fri, 25 Oct 2024 16:21:23 +0300 Subject: [PATCH] [pickers] Fix `DateCalendar` timezone management (#12321) --- codecov.yml | 18 ++++---- .../timezone.DateRangeCalendar.test.tsx | 41 ++++++++++++++++++ .../src/DateCalendar/DayCalendar.tsx | 7 ++- .../tests/timezone.DateCalendar.test.tsx | 43 +++++++++++++++++++ .../src/DateCalendar/useCalendarState.tsx | 29 ++++++++++++- 5 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/DateRangeCalendar/timezone.DateRangeCalendar.test.tsx diff --git a/codecov.yml b/codecov.yml index 4a05503c5b9e..b4af65812615 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,15 +10,15 @@ coverage: adapters: target: 100% paths: - - 'packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts' - - 'packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts' - - 'packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.ts' - - 'packages/x-date-pickers/src/AdapterDateFnsJalaliV3/AdapterDateFnsJalaliV3.ts' - - 'packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts' - - 'packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts' - - 'packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts' - - 'packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.ts' - - 'packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.ts' + - packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts + - packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts + - packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.ts + - packages/x-date-pickers/src/AdapterDateFnsJalaliV3/AdapterDateFnsJalaliV3.ts + - packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts + - packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts + - packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts + - packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.ts + - packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.ts patch: off comment: false diff --git a/packages/x-date-pickers-pro/src/DateRangeCalendar/timezone.DateRangeCalendar.test.tsx b/packages/x-date-pickers-pro/src/DateRangeCalendar/timezone.DateRangeCalendar.test.tsx new file mode 100644 index 000000000000..2d345ab1cf08 --- /dev/null +++ b/packages/x-date-pickers-pro/src/DateRangeCalendar/timezone.DateRangeCalendar.test.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { screen, fireEvent } from '@mui/internal-test-utils'; +import { describeAdapters } from 'test/utils/pickers'; +import { DateRangeCalendar } from './DateRangeCalendar'; + +describe(' - Timezone', () => { + describeAdapters('Timezone prop', DateRangeCalendar, ({ adapter, render }) => { + if (!adapter.isTimezoneCompatible) { + return; + } + + it('should correctly render month days when timezone changes', () => { + function DateCalendarWithControlledTimezone() { + const [timezone, setTimezone] = React.useState('Europe/Paris'); + return ( + + + + + ); + } + render(); + + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + + fireEvent.click(screen.getByRole('button', { name: 'Switch timezone' })); + + // the amount of rendered days should remain the same after changing timezone + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + }); + }); +}); diff --git a/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx b/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx index 8998300b6407..5348469f3c84 100644 --- a/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx +++ b/packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx @@ -534,9 +534,8 @@ export function DayCalendar(inProps: DayCalendarP ]); const weeksToDisplay = React.useMemo(() => { - const currentMonthWithTimezone = utils.setTimezone(currentMonth, timezone); - const toDisplay = utils.getWeekArray(currentMonthWithTimezone); - let nextMonth = utils.addMonths(currentMonthWithTimezone, 1); + const toDisplay = utils.getWeekArray(currentMonth); + let nextMonth = utils.addMonths(currentMonth, 1); while (fixedWeekNumber && toDisplay.length < fixedWeekNumber) { const additionalWeeks = utils.getWeekArray(nextMonth); const hasCommonWeek = utils.isSameDay( @@ -553,7 +552,7 @@ export function DayCalendar(inProps: DayCalendarP nextMonth = utils.addMonths(nextMonth, 1); } return toDisplay; - }, [currentMonth, fixedWeekNumber, utils, timezone]); + }, [currentMonth, fixedWeekNumber, utils]); return ( 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 index 0f00aeee3039..a8624667be44 100644 --- a/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx +++ b/packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx @@ -29,6 +29,49 @@ describe(' - Timezone', () => { expect(actualDate).toEqualDateTime(expectedDate); }); + it('should use "default" timezone for onChange when provided', () => { + const onChange = spy(); + const value = adapter.date('2022-04-25T15:30'); + + render(); + + fireEvent.click(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(adapter.lib === 'dayjs' ? 'UTC' : 'system'); + expect(actualDate).toEqualDateTime(expectedDate); + }); + + it('should correctly render month days when timezone changes', () => { + function DateCalendarWithControlledTimezone() { + const [timezone, setTimezone] = React.useState('Europe/Paris'); + return ( + + + + + ); + } + render(); + + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + + fireEvent.click(screen.getByRole('button', { name: 'Switch timezone' })); + + // the amount of rendered days should remain the same after changing timezone + expect( + screen.getAllByRole('gridcell', { + name: (_, element) => element.nodeName === 'BUTTON', + }).length, + ).to.equal(30); + }); + TIMEZONE_TO_TEST.forEach((timezone) => { describe(`Timezone: ${timezone}`, () => { it('should use timezone prop for onChange when no value is provided', () => { diff --git a/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx b/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx index 29fff983fe3d..5e772c5d742d 100644 --- a/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx +++ b/packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx @@ -43,6 +43,7 @@ export const createCalendarStateReducer = action: | ReducerAction<'finishMonthSwitchingAnimation'> | ReducerAction<'changeMonth', ChangeMonthPayload> + | ReducerAction<'changeMonthTimezone', { newTimezone: string }> | ReducerAction<'changeFocusedDay', ChangeFocusedDayPayload>, ): CalendarState => { switch (action.type) { @@ -54,6 +55,21 @@ export const createCalendarStateReducer = isMonthSwitchingAnimating: !reduceAnimations, }; + case 'changeMonthTimezone': { + const newTimezone = action.newTimezone; + if (utils.getTimezone(state.currentMonth) === newTimezone) { + return state; + } + let newCurrentMonth = utils.setTimezone(state.currentMonth, newTimezone); + if (utils.getMonth(newCurrentMonth) !== utils.getMonth(state.currentMonth)) { + newCurrentMonth = utils.setMonth(newCurrentMonth, utils.getMonth(state.currentMonth)); + } + return { + ...state, + currentMonth: newCurrentMonth, + }; + } + case 'finishMonthSwitchingAnimation': return { ...state, @@ -149,7 +165,9 @@ export const useCalendarState = ( granularity: SECTION_TYPE_GRANULARITY.day, }); }, - [], // eslint-disable-line react-hooks/exhaustive-deps + // We want the `referenceDate` to update on prop and `timezone` change (https://github.com/mui/mui-x/issues/10804) + // eslint-disable-next-line react-hooks/exhaustive-deps + [referenceDateProp, timezone], ); const [calendarState, dispatch] = React.useReducer(reducerFn, { @@ -159,6 +177,15 @@ export const useCalendarState = ( slideDirection: 'left', }); + // Ensure that `calendarState.currentMonth` timezone is updated when `referenceDate` (or timezone changes) + // https://github.com/mui/mui-x/issues/10804 + React.useEffect(() => { + dispatch({ + type: 'changeMonthTimezone', + newTimezone: utils.getTimezone(referenceDate), + }); + }, [referenceDate, utils]); + const handleChangeMonth = React.useCallback( (payload: ChangeMonthPayload) => { dispatch({