Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pickers] Keep the calendar header and content in sync when switching locale #14125

Merged
merged 7 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ export class AdapterDateFnsBase<DateFnsLocale extends DateFnsLocaleBase>
return /a/.test(this.locale.formatLong!.time({ width: 'short' }));
};

// There is no notion of locale on the date in Date Fns.
public setLocaleToValue = (value: Date) => {
return value;
};

public expandFormat = (format: string) => {
const longFormatRegexp = /P+p+|P+|p+|''|'(''|[^'])+('|$)|./g;

Expand Down
18 changes: 9 additions & 9 deletions packages/x-date-pickers/src/AdapterDayjs/AdapterDayjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,6 @@ export class AdapterDayjs implements MuiPickersAdapter<Dayjs, string> {
defaultDayjs.extend(customParseFormatPlugin);
}

private setLocaleToValue = (value: Dayjs) => {
const expectedLocale = this.getCurrentLocaleCode();
if (expectedLocale === value.locale()) {
return value;
}

return value.locale(expectedLocale);
};

private hasUTCPlugin = () => typeof defaultDayjs.utc !== 'undefined';

private hasTimezonePlugin = () => typeof defaultDayjs.tz !== 'undefined';
Expand Down Expand Up @@ -389,6 +380,15 @@ export class AdapterDayjs implements MuiPickersAdapter<Dayjs, string> {
return /A|a/.test(this.getLocaleFormats().LT || '');
};

public setLocaleToValue = (value: Dayjs) => {
const expectedLocale = this.getCurrentLocaleCode();
if (expectedLocale === value.locale()) {
return value;
}

return value.locale(expectedLocale);
};

public expandFormat = (format: string) => {
const localeFormats = this.getLocaleFormats();

Expand Down
18 changes: 9 additions & 9 deletions packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,6 @@ export class AdapterLuxon implements MuiPickersAdapter<DateTime, string> {
this.formats = { ...defaultFormats, ...formats };
}

private setLocaleToValue = (value: DateTime) => {
const expectedLocale = this.getCurrentLocaleCode();
if (expectedLocale === value.locale) {
return value;
}

return value.setLocale(expectedLocale);
};

public date = <T extends string | null | undefined>(
value?: T,
timezone: PickersTimezone = 'default',
Expand Down Expand Up @@ -210,6 +201,15 @@ export class AdapterLuxon implements MuiPickersAdapter<DateTime, string> {
);
};

public setLocaleToValue = (value: DateTime) => {
const expectedLocale = this.getCurrentLocaleCode();
if (expectedLocale === value.locale) {
return value;
}

return value.setLocale(expectedLocale);
};

public expandFormat = (format: string) => {
// Extract escaped section to avoid extending them
const catchEscapedSectionsRegexp = /''|'(''|[^'])+('|$)|[^']*/g;
Expand Down
18 changes: 9 additions & 9 deletions packages/x-date-pickers/src/AdapterMoment/AdapterMoment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,6 @@ export class AdapterMoment implements MuiPickersAdapter<Moment, string> {
this.formats = { ...defaultFormats, ...formats };
}

private setLocaleToValue = (value: Moment) => {
const expectedLocale = this.getCurrentLocaleCode();
if (expectedLocale === value.locale()) {
return value;
}

return value.locale(expectedLocale);
};

private hasTimezonePlugin = () => typeof this.moment.tz !== 'undefined';

private createSystemDate = (value: string | undefined): Moment => {
Expand Down Expand Up @@ -284,6 +275,15 @@ export class AdapterMoment implements MuiPickersAdapter<Moment, string> {
return /A|a/.test(defaultMoment.localeData(this.getCurrentLocaleCode()).longDateFormat('LT'));
};

public setLocaleToValue = (value: Moment) => {
const expectedLocale = this.getCurrentLocaleCode();
if (expectedLocale === value.locale()) {
return value;
}

return value.locale(expectedLocale);
};

public expandFormat = (format: string) => {
// @see https://github.com/moment/moment/blob/develop/src/lib/format/format.js#L6
const localFormattingTokens = /(\[[^[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})|./g;
Expand Down
13 changes: 0 additions & 13 deletions packages/x-date-pickers/src/DateCalendar/DateCalendar.spec.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion packages/x-date-pickers/src/DateCalendar/DayCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ export function DayCalendar<TDate extends PickerValidDate>(inProps: DayCalendarP
key={i.toString()}
variant="caption"
role="columnheader"
aria-label={utils.format(utils.addDays(startOfCurrentWeek, i), 'weekday')}
aria-label={utils.format(weekday, 'weekday')}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea why we were re-doing the day shift here, but I spent some time finding out why my aria-label were not updated in the test whereas the UI was good in the doc 😆

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice optimization. 👍

className={classes.weekDayLabel}
>
{dayOfWeekFormatter(weekday)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import moment, { Moment } from 'moment';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, I just noticed that we had 2 spec files for this component

import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';

<DateCalendar />;
Expand All @@ -8,3 +9,13 @@ import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
<DateCalendar value={new Date()} />;

<DateCalendar value={null} />;

// External components are generic as well
<DateCalendar<Moment>
view="day"
views={['day']}
value={moment()}
minDate={moment()}
maxDate={moment()}
onChange={(date) => date?.format()}
/>;
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as React from 'react';
import { expect } from 'chai';
import { screen } from '@mui/internal-test-utils';
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
import { createPickerRenderer, AdapterName } from 'test/utils/pickers';
import { he } from 'date-fns/locale';
import { screen, createRenderer } from '@mui/internal-test-utils';
import { DateCalendar, dayCalendarClasses } from '@mui/x-date-pickers/DateCalendar';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { createPickerRenderer, AdapterName, availableAdapters } from 'test/utils/pickers';
import { he, fr } from 'date-fns/locale';
import 'dayjs/locale/he';
import 'moment/locale/he';
import 'dayjs/locale/fr';
import 'moment/locale/fr';

const ADAPTERS_TO_USE: AdapterName[] = ['date-fns', 'dayjs', 'luxon', 'moment'];

Expand All @@ -17,11 +20,33 @@ describe('<DateCalendar /> - localization', () => {
adapterName,
});

const { render: renderWithoutWrapper } = createRenderer();

it('should display correct week day labels in Hebrew locale ', () => {
render(<DateCalendar />);

expect(screen.getByText('א')).toBeVisible();
});

it('should correctly switch between locale with week starting in Monday and week starting in Sunday', () => {
const { setProps } = renderWithoutWrapper(
<LocalizationProvider dateAdapter={availableAdapters[adapterName]}>
<DateCalendar />
</LocalizationProvider>,
);

expect(document.querySelector(`.${dayCalendarClasses.weekDayLabel}`)!.ariaLabel).to.equal(
'Sunday',
);

setProps({
adapterLocale: adapterName === 'date-fns' ? fr : 'fr',
});

expect(document.querySelector(`.${dayCalendarClasses.weekDayLabel}`)!.ariaLabel).to.equal(
'lundi',
);
});
});
});
});
3 changes: 2 additions & 1 deletion packages/x-date-pickers/src/internals/utils/date-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export const getWeekdays = <TDate extends PickerValidDate>(
utils: MuiPickersAdapter<TDate>,
date: TDate,
) => {
const start = utils.startOfWeek(date);
const cleanDate = utils.setLocaleToValue(date);
const start = utils.startOfWeek(cleanDate);
return [0, 1, 2, 3, 4, 5, 6].map((diff) => utils.addDays(start, diff));
};
6 changes: 6 additions & 0 deletions packages/x-date-pickers/src/models/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ export interface MuiPickersAdapter<TDate extends PickerValidDate, TLocale = any>
* @returns {boolean} `true` if the current locale is using 12 hours cycle.
*/
is12HourCycleInCurrentLocale(): boolean;
/**
* Set the locale of the adapter to the given value.
* @param {TDate} value The value to set the locale to.
* @returns {TDate} The value with the locale set.
*/
setLocaleToValue(value: TDate): TDate;
/**
* Create a format with no meta-token (e.g: `LLL` or `PP`).
* @param {string} format The format to expand.
Expand Down
14 changes: 13 additions & 1 deletion test/utils/pickers/describeGregorianAdapter/testLocalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ import { cleanText } from 'test/utils/pickers';
import { DescribeGregorianAdapterTestSuite } from './describeGregorianAdapter.types';
import { TEST_DATE_ISO_STRING } from './describeGregorianAdapter.utils';

export const testLocalization: DescribeGregorianAdapterTestSuite = ({ adapter }) => {
export const testLocalization: DescribeGregorianAdapterTestSuite = ({
adapter,
adapterFr,
getLocaleFromDate,
}) => {
const testDateIso = adapter.date(TEST_DATE_ISO_STRING)!;

it('Method: formatNumber', () => {
expect(adapter.formatNumber('1')).to.equal('1');
});

it('Method: setLocaleToValue', () => {
if (getLocaleFromDate) {
const dateEn = adapter.date();
const dateFr = adapterFr.setLocaleToValue(dateEn);
expect(getLocaleFromDate(dateFr)).to.equal('fr');
}
});

it('Method: expandFormat', () => {
const testFormat = (formatKey: keyof AdapterFormats) => {
const formatString = adapter.formats[formatKey];
Expand Down