Skip to content

Commit 0e598b2

Browse files
authored
[pickers] Fix DateCalendar timezone management (#12321)
1 parent ae72b8c commit 0e598b2

File tree

5 files changed

+124
-14
lines changed

5 files changed

+124
-14
lines changed

codecov.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ coverage:
1010
adapters:
1111
target: 100%
1212
paths:
13-
- 'packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts'
14-
- 'packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts'
15-
- 'packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.ts'
16-
- 'packages/x-date-pickers/src/AdapterDateFnsJalaliV3/AdapterDateFnsJalaliV3.ts'
17-
- 'packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts'
18-
- 'packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts'
19-
- 'packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts'
20-
- 'packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.ts'
21-
- 'packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.ts'
13+
- packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts
14+
- packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts
15+
- packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.ts
16+
- packages/x-date-pickers/src/AdapterDateFnsJalaliV3/AdapterDateFnsJalaliV3.ts
17+
- packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts
18+
- packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts
19+
- packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts
20+
- packages/x-date-pickers/src/AdapterMomentHijri/AdapterMomentHijri.ts
21+
- packages/x-date-pickers/src/AdapterMomentJalaali/AdapterMomentJalaali.ts
2222
patch: off
2323

2424
comment: false
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as React from 'react';
2+
import { expect } from 'chai';
3+
import { screen, fireEvent } from '@mui/internal-test-utils';
4+
import { describeAdapters } from 'test/utils/pickers';
5+
import { DateRangeCalendar } from './DateRangeCalendar';
6+
7+
describe('<DateRangeCalendar /> - Timezone', () => {
8+
describeAdapters('Timezone prop', DateRangeCalendar, ({ adapter, render }) => {
9+
if (!adapter.isTimezoneCompatible) {
10+
return;
11+
}
12+
13+
it('should correctly render month days when timezone changes', () => {
14+
function DateCalendarWithControlledTimezone() {
15+
const [timezone, setTimezone] = React.useState('Europe/Paris');
16+
return (
17+
<React.Fragment>
18+
<DateRangeCalendar timezone={timezone} calendars={1} />
19+
<button onClick={() => setTimezone('America/New_York')}>Switch timezone</button>
20+
</React.Fragment>
21+
);
22+
}
23+
render(<DateCalendarWithControlledTimezone />);
24+
25+
expect(
26+
screen.getAllByRole('gridcell', {
27+
name: (_, element) => element.nodeName === 'BUTTON',
28+
}).length,
29+
).to.equal(30);
30+
31+
fireEvent.click(screen.getByRole('button', { name: 'Switch timezone' }));
32+
33+
// the amount of rendered days should remain the same after changing timezone
34+
expect(
35+
screen.getAllByRole('gridcell', {
36+
name: (_, element) => element.nodeName === 'BUTTON',
37+
}).length,
38+
).to.equal(30);
39+
});
40+
});
41+
});

packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,8 @@ export function DayCalendar<TDate extends PickerValidDate>(inProps: DayCalendarP
534534
]);
535535

536536
const weeksToDisplay = React.useMemo(() => {
537-
const currentMonthWithTimezone = utils.setTimezone(currentMonth, timezone);
538-
const toDisplay = utils.getWeekArray(currentMonthWithTimezone);
539-
let nextMonth = utils.addMonths(currentMonthWithTimezone, 1);
537+
const toDisplay = utils.getWeekArray(currentMonth);
538+
let nextMonth = utils.addMonths(currentMonth, 1);
540539
while (fixedWeekNumber && toDisplay.length < fixedWeekNumber) {
541540
const additionalWeeks = utils.getWeekArray(nextMonth);
542541
const hasCommonWeek = utils.isSameDay(
@@ -553,7 +552,7 @@ export function DayCalendar<TDate extends PickerValidDate>(inProps: DayCalendarP
553552
nextMonth = utils.addMonths(nextMonth, 1);
554553
}
555554
return toDisplay;
556-
}, [currentMonth, fixedWeekNumber, utils, timezone]);
555+
}, [currentMonth, fixedWeekNumber, utils]);
557556

558557
return (
559558
<PickersCalendarDayRoot role="grid" aria-labelledby={gridLabelId} className={classes.root}>

packages/x-date-pickers/src/DateCalendar/tests/timezone.DateCalendar.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,49 @@ describe('<DateCalendar /> - Timezone', () => {
2929
expect(actualDate).toEqualDateTime(expectedDate);
3030
});
3131

32+
it('should use "default" timezone for onChange when provided', () => {
33+
const onChange = spy();
34+
const value = adapter.date('2022-04-25T15:30');
35+
36+
render(<DateCalendar value={value} onChange={onChange} timezone="default" />);
37+
38+
fireEvent.click(screen.getByRole('gridcell', { name: '25' }));
39+
const expectedDate = adapter.setDate(value, 25);
40+
41+
// Check the `onChange` value (uses timezone prop)
42+
const actualDate = onChange.lastCall.firstArg;
43+
expect(adapter.getTimezone(actualDate)).to.equal(adapter.lib === 'dayjs' ? 'UTC' : 'system');
44+
expect(actualDate).toEqualDateTime(expectedDate);
45+
});
46+
47+
it('should correctly render month days when timezone changes', () => {
48+
function DateCalendarWithControlledTimezone() {
49+
const [timezone, setTimezone] = React.useState('Europe/Paris');
50+
return (
51+
<React.Fragment>
52+
<DateCalendar timezone={timezone} />
53+
<button onClick={() => setTimezone('America/New_York')}>Switch timezone</button>
54+
</React.Fragment>
55+
);
56+
}
57+
render(<DateCalendarWithControlledTimezone />);
58+
59+
expect(
60+
screen.getAllByRole('gridcell', {
61+
name: (_, element) => element.nodeName === 'BUTTON',
62+
}).length,
63+
).to.equal(30);
64+
65+
fireEvent.click(screen.getByRole('button', { name: 'Switch timezone' }));
66+
67+
// the amount of rendered days should remain the same after changing timezone
68+
expect(
69+
screen.getAllByRole('gridcell', {
70+
name: (_, element) => element.nodeName === 'BUTTON',
71+
}).length,
72+
).to.equal(30);
73+
});
74+
3275
TIMEZONE_TO_TEST.forEach((timezone) => {
3376
describe(`Timezone: ${timezone}`, () => {
3477
it('should use timezone prop for onChange when no value is provided', () => {

packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const createCalendarStateReducer =
4343
action:
4444
| ReducerAction<'finishMonthSwitchingAnimation'>
4545
| ReducerAction<'changeMonth', ChangeMonthPayload<TDate>>
46+
| ReducerAction<'changeMonthTimezone', { newTimezone: string }>
4647
| ReducerAction<'changeFocusedDay', ChangeFocusedDayPayload<TDate>>,
4748
): CalendarState<TDate> => {
4849
switch (action.type) {
@@ -54,6 +55,21 @@ export const createCalendarStateReducer =
5455
isMonthSwitchingAnimating: !reduceAnimations,
5556
};
5657

58+
case 'changeMonthTimezone': {
59+
const newTimezone = action.newTimezone;
60+
if (utils.getTimezone(state.currentMonth) === newTimezone) {
61+
return state;
62+
}
63+
let newCurrentMonth = utils.setTimezone(state.currentMonth, newTimezone);
64+
if (utils.getMonth(newCurrentMonth) !== utils.getMonth(state.currentMonth)) {
65+
newCurrentMonth = utils.setMonth(newCurrentMonth, utils.getMonth(state.currentMonth));
66+
}
67+
return {
68+
...state,
69+
currentMonth: newCurrentMonth,
70+
};
71+
}
72+
5773
case 'finishMonthSwitchingAnimation':
5874
return {
5975
...state,
@@ -149,7 +165,9 @@ export const useCalendarState = <TDate extends PickerValidDate>(
149165
granularity: SECTION_TYPE_GRANULARITY.day,
150166
});
151167
},
152-
[], // eslint-disable-line react-hooks/exhaustive-deps
168+
// We want the `referenceDate` to update on prop and `timezone` change (https://github.com/mui/mui-x/issues/10804)
169+
// eslint-disable-next-line react-hooks/exhaustive-deps
170+
[referenceDateProp, timezone],
153171
);
154172

155173
const [calendarState, dispatch] = React.useReducer(reducerFn, {
@@ -159,6 +177,15 @@ export const useCalendarState = <TDate extends PickerValidDate>(
159177
slideDirection: 'left',
160178
});
161179

180+
// Ensure that `calendarState.currentMonth` timezone is updated when `referenceDate` (or timezone changes)
181+
// https://github.com/mui/mui-x/issues/10804
182+
React.useEffect(() => {
183+
dispatch({
184+
type: 'changeMonthTimezone',
185+
newTimezone: utils.getTimezone(referenceDate),
186+
});
187+
}, [referenceDate, utils]);
188+
162189
const handleChangeMonth = React.useCallback(
163190
(payload: ChangeMonthPayload<TDate>) => {
164191
dispatch({

0 commit comments

Comments
 (0)