Skip to content

Commit

Permalink
[pickers] Add date-fns@3.x adapter (@LukasTy) (#11659)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas <llukas.tyla@gmail.com>
Co-authored-by: Lukas <llukas.tyla@gmail.com>
Co-authored-by: Michel Engelen <32863416+michelengelen@users.noreply.github.com>
  • Loading branch information
3 people committed Jan 11, 2024
1 parent e923c98 commit 5cfa51d
Show file tree
Hide file tree
Showing 23 changed files with 792 additions and 222 deletions.
14 changes: 14 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ module.exports = function getBabelConfig(api) {
],
];

if (process.env.NODE_ENV === 'test') {
// We replace `date-fns` imports with an aliased `date-fns@v3` version installed as `date-fns-v3` for tests.
// The plugin is patched to only run on `AdapterDateFnsV3.ts`.
// TODO: remove when we upgrade to date-fns v3 by default.
plugins.push([
'babel-plugin-replace-imports',
{
test: /date-fns/i,
replacer: 'date-fns-v3',
ignoreFilenames: 'AdapterDateFns.ts',
},
]);
}

if (process.env.NODE_ENV === 'production') {
plugins.push(...productionPlugins);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import InputBase from '@mui/material/InputBase';
import locale from 'date-fns/locale/en-US';
import { enUS as locale } from 'date-fns/locale';
import { styled } from '@mui/material/styles';

function buildApplyDateFilterFn(filterItem, compareFn, showTime = false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import InputBase, { InputBaseProps } from '@mui/material/InputBase';
import locale from 'date-fns/locale/en-US';
import { enUS as locale } from 'date-fns/locale';
import { styled } from '@mui/material/styles';
import { TextFieldProps } from '@mui/material/TextField';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as React from 'react';
import de from 'date-fns/locale/de';
import enGB from 'date-fns/locale/en-GB';
import zhCN from 'date-fns/locale/zh-CN';
import { de, enGB, zhCN } from 'date-fns/locale';
import Stack from '@mui/material/Stack';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as React from 'react';
import de from 'date-fns/locale/de';
import enGB from 'date-fns/locale/en-GB';
import zhCN from 'date-fns/locale/zh-CN';
import { de, enGB, zhCN } from 'date-fns/locale';
import Stack from '@mui/material/Stack';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
Expand Down
14 changes: 14 additions & 0 deletions docs/data/date-pickers/adapters-locale/adapters-locale.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,23 @@ import 'dayjs/locale/de';

For `date-fns`, import the locale and pass it to `LocalizationProvider`:

:::info
Both `date-fns` major versions (v2.x and v3.x) are supported.

A single adapter can not work for both `date-fns` v2.x and v3.x, because the way functions are exported has been changed in v3.x.

To use `date-fns` v3.x, you will have to import the adapter from `@mui/x-date-pickers/AdapterDateFnsV3` instead of `@mui/x-date-pickers/AdapterDateFns`.
:::

```tsx
// with date-fns v2.x
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
// with date-fns v3.x
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
// with date-fns v2.x
import de from 'date-fns/locale/de';
// with date-fns v3.x
import { de } from 'date-fns/locale/de';

<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={de}>
{children}
Expand Down
1 change: 1 addition & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"core-js": "^2.6.12",
"cross-env": "^7.0.3",
"date-fns": "^2.30.0",
"date-fns-v3": "https://registry.npmjs.org/date-fns/-/date-fns-3.2.0.tgz",
"date-fns-jalali": "^2.21.3-1",
"dayjs": "^1.11.10",
"doctrine": "^3.0.0",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"babel-plugin-module-resolver": "^4.1.0",
"babel-plugin-optimize-clsx": "^2.6.2",
"babel-plugin-react-remove-properties": "^0.3.0",
"babel-plugin-replace-imports": "^1.0.2",
"babel-plugin-search-and-replace": "^1.1.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-plugin-transform-rename-import": "^2.3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/x-date-pickers-pro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.8.6",
"@mui/system": "^5.8.0",
"date-fns": "^2.25.0",
"date-fns": "^2.25.0 || ^3.2.0",
"date-fns-jalali": "^2.13.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
Expand Down
1 change: 1 addition & 0 deletions packages/x-date-pickers-pro/src/AdapterDateFnsV3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
2 changes: 1 addition & 1 deletion packages/x-date-pickers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.8.6",
"@mui/system": "^5.8.0",
"date-fns": "^2.25.0",
"date-fns": "^2.25.0 || ^3.2.0",
"date-fns-jalali": "^2.13.0-0",
"dayjs": "^1.10.7",
"luxon": "^3.0.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import {
describeGregorianAdapter,
TEST_DATE_ISO_STRING,
} from 'test/utils/pickers';
import enUS from 'date-fns/locale/en-US';
import fr from 'date-fns/locale/fr';
import de from 'date-fns/locale/de';
import ru from 'date-fns/locale/ru';
import { enUS, fr, de, ru } from 'date-fns/locale';

describe('<AdapterDateFns />', () => {
describeGregorianAdapter(AdapterDateFns, {
Expand Down
220 changes: 15 additions & 205 deletions packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,116 +56,11 @@ import isWithinInterval from 'date-fns/isWithinInterval';
import defaultLocale from 'date-fns/locale/en-US';
// @ts-ignore
import longFormatters from 'date-fns/_lib/format/longFormatters';
import {
AdapterFormats,
AdapterOptions,
AdapterUnits,
DateBuilderReturnType,
FieldFormatTokenMap,
MuiPickersAdapter,
} from '../models';
import { AdapterFormats, AdapterOptions, AdapterUnits, MuiPickersAdapter } from '../models';
import { AdapterDateFnsBase } from '../AdapterDateFnsBase';

type DateFnsLocale = typeof defaultLocale;

const formatTokenMap: FieldFormatTokenMap = {
// Year
y: { sectionType: 'year', contentType: 'digit', maxLength: 4 },
yy: 'year',
yyy: { sectionType: 'year', contentType: 'digit', maxLength: 4 },
yyyy: 'year',

// Month
M: { sectionType: 'month', contentType: 'digit', maxLength: 2 },
MM: 'month',
MMMM: { sectionType: 'month', contentType: 'letter' },
MMM: { sectionType: 'month', contentType: 'letter' },
L: { sectionType: 'month', contentType: 'digit', maxLength: 2 },
LL: 'month',
LLL: { sectionType: 'month', contentType: 'letter' },
LLLL: { sectionType: 'month', contentType: 'letter' },

// Day of the month
d: { sectionType: 'day', contentType: 'digit', maxLength: 2 },
dd: 'day',
do: { sectionType: 'day', contentType: 'digit-with-letter' },

// Day of the week
E: { sectionType: 'weekDay', contentType: 'letter' },
EE: { sectionType: 'weekDay', contentType: 'letter' },
EEE: { sectionType: 'weekDay', contentType: 'letter' },
EEEE: { sectionType: 'weekDay', contentType: 'letter' },
EEEEE: { sectionType: 'weekDay', contentType: 'letter' },
i: { sectionType: 'weekDay', contentType: 'digit', maxLength: 1 },
ii: 'weekDay',
iii: { sectionType: 'weekDay', contentType: 'letter' },
iiii: { sectionType: 'weekDay', contentType: 'letter' },
e: { sectionType: 'weekDay', contentType: 'digit', maxLength: 1 },
ee: 'weekDay',
eee: { sectionType: 'weekDay', contentType: 'letter' },
eeee: { sectionType: 'weekDay', contentType: 'letter' },
eeeee: { sectionType: 'weekDay', contentType: 'letter' },
eeeeee: { sectionType: 'weekDay', contentType: 'letter' },
c: { sectionType: 'weekDay', contentType: 'digit', maxLength: 1 },
cc: 'weekDay',
ccc: { sectionType: 'weekDay', contentType: 'letter' },
cccc: { sectionType: 'weekDay', contentType: 'letter' },
ccccc: { sectionType: 'weekDay', contentType: 'letter' },
cccccc: { sectionType: 'weekDay', contentType: 'letter' },

// Meridiem
a: 'meridiem',
aa: 'meridiem',
aaa: 'meridiem',

// Hours
H: { sectionType: 'hours', contentType: 'digit', maxLength: 2 },
HH: 'hours',
h: { sectionType: 'hours', contentType: 'digit', maxLength: 2 },
hh: 'hours',

// Minutes
m: { sectionType: 'minutes', contentType: 'digit', maxLength: 2 },
mm: 'minutes',

// Seconds
s: { sectionType: 'seconds', contentType: 'digit', maxLength: 2 },
ss: 'seconds',
};

const defaultFormats: AdapterFormats = {
year: 'yyyy',
month: 'LLLL',
monthShort: 'MMM',
dayOfMonth: 'd',
weekday: 'EEEE',
weekdayShort: 'EEEEEE',
hours24h: 'HH',
hours12h: 'hh',
meridiem: 'aa',
minutes: 'mm',
seconds: 'ss',

fullDate: 'PP',
fullDateWithWeekday: 'PPPP',
keyboardDate: 'P',
shortDate: 'MMM d',
normalDate: 'd MMMM',
normalDateWithWeekday: 'EEE, MMM d',
monthAndYear: 'LLLL yyyy',
monthAndDate: 'MMMM d',

fullTime: 'p',
fullTime12h: 'hh:mm aa',
fullTime24h: 'HH:mm',

fullDateTime: 'PP p',
fullDateTime12h: 'PP hh:mm aa',
fullDateTime24h: 'PP HH:mm',
keyboardDateTime: 'P p',
keyboardDateTime12h: 'P hh:mm aa',
keyboardDateTime24h: 'P HH:mm',
};

/**
* Based on `@date-io/date-fns`
*
Expand All @@ -191,55 +86,21 @@ const defaultFormats: AdapterFormats = {
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export class AdapterDateFns implements MuiPickersAdapter<Date, DateFnsLocale> {
public isMUIAdapter = true;

public isTimezoneCompatible = false;

public lib = 'date-fns';

public locale?: DateFnsLocale;

public formats: AdapterFormats;

public formatTokenMap = formatTokenMap;

public escapedCharacters = { start: "'", end: "'" };

export class AdapterDateFns
extends AdapterDateFnsBase<DateFnsLocale>
implements MuiPickersAdapter<Date, DateFnsLocale>
{
constructor({ locale, formats }: AdapterOptions<DateFnsLocale, never> = {}) {
this.locale = locale;
this.formats = { ...defaultFormats, ...formats };
}

public date = (value?: any) => {
if (typeof value === 'undefined') {
return new Date();
if (typeof addDays !== 'function') {
throw new Error(
[
'MUI: The `date-fns` package v3.x is not compatible with this adapter.',
'Please, install v2.x of the package or use the `AdapterDateFnsV3` instead.',
].join('\n'),
);
}

if (value === null) {
return null;
}

return new Date(value);
};

public dateWithTimezone = <T extends string | null | undefined>(
value: T,
): DateBuilderReturnType<T, Date> => {
return <DateBuilderReturnType<T, Date>>this.date(value);
};

public getTimezone = (): string => {
return 'default';
};

public setTimezone = (value: Date): Date => {
return value;
};

public toJsDate = (value: Date) => {
return value;
};
super({ locale: locale ?? defaultLocale, formats, longFormatters });
}

public parseISO = (isoString: string) => {
return parseISO(isoString);
Expand All @@ -257,49 +118,6 @@ export class AdapterDateFns implements MuiPickersAdapter<Date, DateFnsLocale> {
return dateFnsParse(value, format, new Date(), { locale: this.locale });
};

public getCurrentLocaleCode = () => {
return this.locale?.code || 'en-US';
};

// Note: date-fns input types are more lenient than this adapter, so we need to expose our more
// strict signature and delegate to the more lenient signature. Otherwise, we have downstream type errors upon usage.
public is12HourCycleInCurrentLocale = () => {
if (this.locale) {
return /a/.test(this.locale.formatLong!.time());
}

// By default, date-fns is using en-US locale with am/pm enabled
return true;
};

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

// @see https://github.com/date-fns/date-fns/blob/master/src/format/index.js#L31
return format
.match(longFormatRegexp)!
.map((token: string) => {
const firstCharacter = token[0];
if (firstCharacter === 'p' || firstCharacter === 'P') {
const longFormatter = longFormatters[firstCharacter];
const locale = this.locale || defaultLocale;
return longFormatter(token, locale.formatLong, {});
}
return token;
})
.join('');
};

public getFormatHelperText = (format: string) => {
return this.expandFormat(format)
.replace(/(aaa|aa|a)/g, '(a|p)m')
.toLocaleLowerCase();
};

public isNull = (value: Date | null) => {
return value === null;
};

public isValid = (value: any) => {
return isValid(this.date(value));
};
Expand All @@ -312,10 +130,6 @@ export class AdapterDateFns implements MuiPickersAdapter<Date, DateFnsLocale> {
return dateFnsFormat(value, formatString, { locale: this.locale });
};

public formatNumber = (numberToFormat: string) => {
return numberToFormat;
};

public getDiff = (value: Date, comparing: Date | string, unit?: AdapterUnits) => {
switch (unit) {
case 'years':
Expand Down Expand Up @@ -587,8 +401,4 @@ export class AdapterDateFns implements MuiPickersAdapter<Date, DateFnsLocale> {

return years;
};

public getMeridiemText = (ampm: 'am' | 'pm') => {
return ampm === 'am' ? 'AM' : 'PM';
};
}
Loading

0 comments on commit 5cfa51d

Please sign in to comment.