Skip to content

Commit

Permalink
[pickers] Fix DateCalendar timezone management (@LukasTy) (#15119)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukas Tyla <[email protected]>
  • Loading branch information
2 people authored and web-flow committed Oct 25, 2024
1 parent d4fe257 commit cd964b5
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 14 deletions.
18 changes: 9 additions & 9 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ coverage:
adapters:
target: 100%
paths:
- 'packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts'
- 'packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.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'
ignore:
- '**/*.test.tsx'
- 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
Original file line number Diff line number Diff line change
@@ -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('<DateRangeCalendar /> - 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 (
<React.Fragment>
<DateRangeCalendar timezone={timezone} calendars={1} />
<button onClick={() => setTimezone('America/New_York')}>Switch timezone</button>
</React.Fragment>
);
}
render(<DateCalendarWithControlledTimezone />);

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);
});
});
});
7 changes: 3 additions & 4 deletions packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,8 @@ export function DayCalendar<TDate>(inProps: DayCalendarProps<TDate>) {
]);

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(
Expand All @@ -552,7 +551,7 @@ export function DayCalendar<TDate>(inProps: DayCalendarProps<TDate>) {
nextMonth = utils.addMonths(nextMonth, 1);
}
return toDisplay;
}, [currentMonth, fixedWeekNumber, utils, timezone]);
}, [currentMonth, fixedWeekNumber, utils]);

return (
<PickersCalendarDayRoot role="grid" aria-labelledby={gridLabelId} className={classes.root}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,49 @@ describe('<DateCalendar /> - 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(<DateCalendar value={value} onChange={onChange} timezone="default" />);

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 (
<React.Fragment>
<DateCalendar timezone={timezone} />
<button onClick={() => setTimezone('America/New_York')}>Switch timezone</button>
</React.Fragment>
);
}
render(<DateCalendarWithControlledTimezone />);

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', () => {
Expand Down
29 changes: 28 additions & 1 deletion packages/x-date-pickers/src/DateCalendar/useCalendarState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const createCalendarStateReducer =
action:
| ReducerAction<'finishMonthSwitchingAnimation'>
| ReducerAction<'changeMonth', ChangeMonthPayload<TDate>>
| ReducerAction<'changeMonthTimezone', { newTimezone: string }>
| ReducerAction<'changeFocusedDay', ChangeFocusedDayPayload<TDate>>,
): CalendarState<TDate> => {
switch (action.type) {
Expand All @@ -53,6 +54,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,
Expand Down Expand Up @@ -156,7 +172,9 @@ export const useCalendarState = <TDate extends unknown>(params: 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, {
Expand All @@ -166,6 +184,15 @@ export const useCalendarState = <TDate extends unknown>(params: 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<TDate>) => {
dispatch({
Expand Down

0 comments on commit cd964b5

Please sign in to comment.