From a69432d95aedcd8888433fcee071e9d2a51b2f8c Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 11 Jan 2024 13:37:57 +0200 Subject: [PATCH] [pickers] Add date-fns@3.x adapter (#11462) Signed-off-by: Lukas Co-authored-by: Michel Engelen <32863416+michelengelen@users.noreply.github.com> --- babel.config.js | 11 + .../custom-columns/EditingWithDatePickers.js | 2 +- .../custom-columns/EditingWithDatePickers.tsx | 2 +- .../adapters-locale/LocalizationDateFns.js | 4 +- .../adapters-locale/LocalizationDateFns.tsx | 4 +- .../adapters-locale/adapters-locale.md | 17 + docs/package.json | 1 + package.json | 1 + packages/x-date-pickers-pro/package.json | 2 +- .../src/AdapterDateFnsV3/index.ts | 1 + packages/x-date-pickers/package.json | 2 +- .../AdapterDateFns/AdapterDateFns.test.tsx | 5 +- .../src/AdapterDateFns/AdapterDateFns.ts | 198 +--------- .../AdapterDateFnsBase/AdapterDateFnsBase.ts | 247 +++++++++++++ .../src/AdapterDateFnsBase/index.ts | 1 + .../AdapterDateFnsJalali.test.tsx | 2 +- .../AdapterDateFnsV3.test.tsx | 11 + .../src/AdapterDateFnsV3/AdapterDateFnsV3.ts | 337 ++++++++++++++++++ .../src/AdapterDateFnsV3/index.ts | 1 + .../src/internals/models/helpers.ts | 2 + .../babel-plugin-replace-imports+1.0.2.patch | 9 + .../describeGregorianAdapter.ts | 2 +- yarn.lock | 32 +- 23 files changed, 694 insertions(+), 200 deletions(-) create mode 100644 packages/x-date-pickers-pro/src/AdapterDateFnsV3/index.ts create mode 100644 packages/x-date-pickers/src/AdapterDateFnsBase/AdapterDateFnsBase.ts create mode 100644 packages/x-date-pickers/src/AdapterDateFnsBase/index.ts create mode 100644 packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.test.tsx create mode 100644 packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts create mode 100644 packages/x-date-pickers/src/AdapterDateFnsV3/index.ts create mode 100644 patches/babel-plugin-replace-imports+1.0.2.patch diff --git a/babel.config.js b/babel.config.js index fd1032276bcfe..99b3e5911f757 100644 --- a/babel.config.js +++ b/babel.config.js @@ -83,6 +83,17 @@ module.exports = function getBabelConfig(api) { if (process.env.NODE_ENV === 'test') { plugins.push(['@babel/plugin-transform-export-namespace-from']); + // 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') { diff --git a/docs/data/data-grid/custom-columns/EditingWithDatePickers.js b/docs/data/data-grid/custom-columns/EditingWithDatePickers.js index c05f6a6dd1f4f..3278ebe5189bf 100644 --- a/docs/data/data-grid/custom-columns/EditingWithDatePickers.js +++ b/docs/data/data-grid/custom-columns/EditingWithDatePickers.js @@ -16,7 +16,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'; const dateAdapter = new AdapterDateFns({ locale }); diff --git a/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx b/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx index c380a85f6e071..d51f0fea6b871 100644 --- a/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx +++ b/docs/data/data-grid/custom-columns/EditingWithDatePickers.tsx @@ -21,7 +21,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'; diff --git a/docs/data/date-pickers/adapters-locale/LocalizationDateFns.js b/docs/data/date-pickers/adapters-locale/LocalizationDateFns.js index 76dcfe37d14b8..c47cf69443d3a 100644 --- a/docs/data/date-pickers/adapters-locale/LocalizationDateFns.js +++ b/docs/data/date-pickers/adapters-locale/LocalizationDateFns.js @@ -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'; diff --git a/docs/data/date-pickers/adapters-locale/LocalizationDateFns.tsx b/docs/data/date-pickers/adapters-locale/LocalizationDateFns.tsx index a4cedaa05dba3..439cf7f041319 100644 --- a/docs/data/date-pickers/adapters-locale/LocalizationDateFns.tsx +++ b/docs/data/date-pickers/adapters-locale/LocalizationDateFns.tsx @@ -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'; diff --git a/docs/data/date-pickers/adapters-locale/adapters-locale.md b/docs/data/date-pickers/adapters-locale/adapters-locale.md index 0435876cf163e..31aeb938d6df9 100644 --- a/docs/data/date-pickers/adapters-locale/adapters-locale.md +++ b/docs/data/date-pickers/adapters-locale/adapters-locale.md @@ -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'; {children} @@ -280,7 +294,10 @@ dayjs.updateLocale('en', { For `date-fns`, use the `setDefaultOptions` utility: ```ts +// with date-fns v2.x import setDefaultOptions from 'date-fns/setDefaultOptions'; +// with date-fns v3.x +import { setDefaultOptions } from 'date-fns/setDefaultOptions'; setDefaultOptions({ // Sunday = 0, Monday = 1. diff --git a/docs/package.json b/docs/package.json index 6b1dc0692fad3..1d1d98f5b8168 100644 --- a/docs/package.json +++ b/docs/package.json @@ -52,6 +52,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", diff --git a/package.json b/package.json index 0adb857ac6903..30fe2dc5ecf06 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "babel-plugin-module-resolver": "^5.0.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", diff --git a/packages/x-date-pickers-pro/package.json b/packages/x-date-pickers-pro/package.json index dc93fb9c6d621..4ebf11954bc76 100644 --- a/packages/x-date-pickers-pro/package.json +++ b/packages/x-date-pickers-pro/package.json @@ -56,7 +56,7 @@ "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^5.8.6", - "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", diff --git a/packages/x-date-pickers-pro/src/AdapterDateFnsV3/index.ts b/packages/x-date-pickers-pro/src/AdapterDateFnsV3/index.ts new file mode 100644 index 0000000000000..7291f164d3f4d --- /dev/null +++ b/packages/x-date-pickers-pro/src/AdapterDateFnsV3/index.ts @@ -0,0 +1 @@ +export { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3'; diff --git a/packages/x-date-pickers/package.json b/packages/x-date-pickers/package.json index d7c16cb88c043..f7b2cabbc9979 100644 --- a/packages/x-date-pickers/package.json +++ b/packages/x-date-pickers/package.json @@ -58,7 +58,7 @@ "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^5.8.6", - "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", diff --git a/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx b/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx index a291ec5f0bbd7..b844cbbc195e7 100644 --- a/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx +++ b/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.test.tsx @@ -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('', () => { describeGregorianAdapter(AdapterDateFns, { diff --git a/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts b/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts index 0f8569dc94851..2fdf221e35303 100644 --- a/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts +++ b/packages/x-date-pickers/src/AdapterDateFns/AdapterDateFns.ts @@ -44,109 +44,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, - DateBuilderReturnType, - FieldFormatTokenMap, - MuiPickersAdapter, -} from '../models'; +import { AdapterFormats, AdapterOptions, 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', - keyboardDate: 'P', - shortDate: 'MMM d', - normalDate: 'd MMMM', - normalDateWithWeekday: 'EEE, MMM d', - - fullTime: 'p', - fullTime12h: 'hh:mm aa', - fullTime24h: 'HH:mm', - - keyboardDateTime: 'P p', - keyboardDateTime12h: 'P hh:mm aa', - keyboardDateTime24h: 'P HH:mm', -}; - /** * Based on `@date-io/date-fns` * @@ -172,54 +74,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 { - 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 + implements MuiPickersAdapter +{ constructor({ locale, formats }: AdapterOptions = {}) { - this.locale = locale; - this.formats = { ...defaultFormats, ...formats }; - } - - public date = ( - value?: T, - ): DateBuilderReturnType => { - type R = DateBuilderReturnType; - if (typeof value === 'undefined') { - return new Date(); - } - - if (value === null) { - return null; + 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'), + ); } - - return new Date(value); - }; - - public getInvalidDate = () => new Date('Invalid Date'); - - 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 parse = (value: string, format: string) => { if (value === '') { @@ -229,39 +98,6 @@ export class AdapterDateFns implements MuiPickersAdapter { 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 isValid = (value: Date | null) => { if (value == null) { return false; @@ -278,10 +114,6 @@ export class AdapterDateFns implements MuiPickersAdapter { return dateFnsFormat(value, formatString, { locale: this.locale }); }; - public formatNumber = (numberToFormat: string) => { - return numberToFormat; - }; - public isEqual = (value: Date | null, comparing: Date | null) => { if (value === null && comparing === null) { return true; diff --git a/packages/x-date-pickers/src/AdapterDateFnsBase/AdapterDateFnsBase.ts b/packages/x-date-pickers/src/AdapterDateFnsBase/AdapterDateFnsBase.ts new file mode 100644 index 0000000000000..2207845c9cf38 --- /dev/null +++ b/packages/x-date-pickers/src/AdapterDateFnsBase/AdapterDateFnsBase.ts @@ -0,0 +1,247 @@ +/* eslint-disable class-methods-use-this */ +import { + AdapterFormats, + AdapterOptions, + DateBuilderReturnType, + FieldFormatTokenMap, + MuiPickersAdapter, +} from '../models'; +import { MakeRequired } from '../internals/models/helpers'; + +type DateFnsLocaleBase = { + formatLong?: { + date?: any; + time?: any; + dateTime?: any; + }; + code?: string; +}; + +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', + keyboardDate: 'P', + shortDate: 'MMM d', + normalDate: 'd MMMM', + normalDateWithWeekday: 'EEE, MMM d', + + fullTime: 'p', + fullTime12h: 'hh:mm aa', + fullTime24h: 'HH:mm', + + keyboardDateTime: 'P p', + keyboardDateTime12h: 'P hh:mm aa', + keyboardDateTime24h: 'P HH:mm', +}; + +type DateFnsAdapterBaseOptions = MakeRequired< + AdapterOptions, + 'locale' +> & { + longFormatters: Record<'p' | 'P', (token: string, formatLong: any) => string>; +}; + +/** + * Based on `@date-io/date-fns` + * + * MIT License + * + * Copyright (c) 2017 Dmitriy Kovalenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export class AdapterDateFnsBase + implements + Pick< + MuiPickersAdapter, + | 'date' + | 'getInvalidDate' + | 'getTimezone' + | 'setTimezone' + | 'toJsDate' + | 'getCurrentLocaleCode' + | 'is12HourCycleInCurrentLocale' + | 'expandFormat' + | 'formatNumber' + > +{ + public isMUIAdapter = true; + + public isTimezoneCompatible = false; + + public lib = 'date-fns'; + + public locale: DateFnsLocale; + + public formats: AdapterFormats; + + public formatTokenMap = formatTokenMap; + + public escapedCharacters = { start: "'", end: "'" }; + + public longFormatters: DateFnsAdapterBaseOptions['longFormatters']; + + constructor(props: DateFnsAdapterBaseOptions) { + const { locale, formats, longFormatters } = props; + this.locale = locale; + this.formats = { ...defaultFormats, ...formats }; + this.longFormatters = longFormatters; + } + + public date = ( + value?: T, + ): DateBuilderReturnType => { + type R = DateBuilderReturnType; + if (typeof value === 'undefined') { + return new Date(); + } + + if (value === null) { + return null; + } + + return new Date(value); + }; + + public getInvalidDate = () => new Date('Invalid Date'); + + public getTimezone = (): string => { + return 'default'; + }; + + public setTimezone = (value: Date): Date => { + return value; + }; + + public toJsDate = (value: Date) => { + return value; + }; + + 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({ width: 'short' })); + } + + // 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 = this.longFormatters[firstCharacter]; + return longFormatter(token, this.locale.formatLong); + } + return token; + }) + .join(''); + }; + + public formatNumber = (numberToFormat: string) => { + return numberToFormat; + }; +} diff --git a/packages/x-date-pickers/src/AdapterDateFnsBase/index.ts b/packages/x-date-pickers/src/AdapterDateFnsBase/index.ts new file mode 100644 index 0000000000000..101a15aa9ab3e --- /dev/null +++ b/packages/x-date-pickers/src/AdapterDateFnsBase/index.ts @@ -0,0 +1 @@ +export { AdapterDateFnsBase } from './AdapterDateFnsBase'; diff --git a/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx b/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx index 2d4d6acb5f15f..268f1aeeaf6f8 100644 --- a/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx +++ b/packages/x-date-pickers/src/AdapterDateFnsJalali/AdapterDateFnsJalali.test.tsx @@ -9,7 +9,7 @@ import { expectInputValue, describeJalaliAdapter, } from 'test/utils/pickers'; -import enUS from 'date-fns/locale/en-US'; +import { enUS } from 'date-fns/locale'; import faIR from 'date-fns-jalali/locale/fa-IR'; import faJalaliIR from 'date-fns-jalali/locale/fa-jalali-IR'; import { AdapterMomentJalaali } from '@mui/x-date-pickers/AdapterMomentJalaali'; diff --git a/packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.test.tsx b/packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.test.tsx new file mode 100644 index 0000000000000..f84af39c4472e --- /dev/null +++ b/packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.test.tsx @@ -0,0 +1,11 @@ +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3'; +import { describeGregorianAdapter } from 'test/utils/pickers/describeGregorianAdapter'; +import { fr } from 'date-fns/locale'; + +describe('', () => { + describeGregorianAdapter(AdapterDateFns, { + formatDateTime: 'yyyy-MM-dd HH:mm:ss', + setDefaultTimezone: () => {}, + frenchLocale: fr, + }); +}); diff --git a/packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts b/packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts new file mode 100644 index 0000000000000..5819ca3b7559b --- /dev/null +++ b/packages/x-date-pickers/src/AdapterDateFnsV3/AdapterDateFnsV3.ts @@ -0,0 +1,337 @@ +/* eslint-disable class-methods-use-this */ +// TODO remove when date-fns-v3 is the default +// @ts-nocheck +import { addDays } from 'date-fns/addDays'; +import { addSeconds } from 'date-fns/addSeconds'; +import { addMinutes } from 'date-fns/addMinutes'; +import { addHours } from 'date-fns/addHours'; +import { addWeeks } from 'date-fns/addWeeks'; +import { addMonths } from 'date-fns/addMonths'; +import { addYears } from 'date-fns/addYears'; +import { endOfDay } from 'date-fns/endOfDay'; +import { endOfWeek } from 'date-fns/endOfWeek'; +import { endOfYear } from 'date-fns/endOfYear'; +// @ts-ignore TODO remove when date-fns-v3 is the default +import { format as dateFnsFormat, longFormatters } from 'date-fns/format'; +import { getDate } from 'date-fns/getDate'; +import { getDaysInMonth } from 'date-fns/getDaysInMonth'; +import { getHours } from 'date-fns/getHours'; +import { getMinutes } from 'date-fns/getMinutes'; +import { getMonth } from 'date-fns/getMonth'; +import { getSeconds } from 'date-fns/getSeconds'; +import { getMilliseconds } from 'date-fns/getMilliseconds'; +import { getWeek } from 'date-fns/getWeek'; +import { getYear } from 'date-fns/getYear'; +import { isAfter } from 'date-fns/isAfter'; +import { isBefore } from 'date-fns/isBefore'; +import { isEqual } from 'date-fns/isEqual'; +import { isSameDay } from 'date-fns/isSameDay'; +import { isSameYear } from 'date-fns/isSameYear'; +import { isSameMonth } from 'date-fns/isSameMonth'; +import { isSameHour } from 'date-fns/isSameHour'; +import { isValid } from 'date-fns/isValid'; +import { parse as dateFnsParse } from 'date-fns/parse'; +import { setDate } from 'date-fns/setDate'; +import { setHours } from 'date-fns/setHours'; +import { setMinutes } from 'date-fns/setMinutes'; +import { setMonth } from 'date-fns/setMonth'; +import { setSeconds } from 'date-fns/setSeconds'; +import { setMilliseconds } from 'date-fns/setMilliseconds'; +import { setYear } from 'date-fns/setYear'; +import { startOfDay } from 'date-fns/startOfDay'; +import { startOfMonth } from 'date-fns/startOfMonth'; +import { endOfMonth } from 'date-fns/endOfMonth'; +import { startOfWeek } from 'date-fns/startOfWeek'; +import { startOfYear } from 'date-fns/startOfYear'; +import { isWithinInterval } from 'date-fns/isWithinInterval'; +import { enUS } from 'date-fns/locale/en-US'; +// date-fns v2 does not export types +// @ts-ignore TODO remove when date-fns-v3 is the default +import { Locale as DateFnsLocale } from 'date-fns/locale/types'; +import { AdapterFormats, AdapterOptions, MuiPickersAdapter } from '../models'; +import { AdapterDateFnsBase } from '../AdapterDateFnsBase'; + +/** + * Based on `@date-io/date-fns` + * + * MIT License + * + * Copyright (c) 2017 Dmitriy Kovalenko + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export class AdapterDateFns + extends AdapterDateFnsBase + implements MuiPickersAdapter +{ + constructor({ locale, formats }: AdapterOptions = {}) { + if (typeof addDays !== 'function') { + throw new Error( + [ + `MUI: The \`date-fns\` package v2.x is not compatible with this adapter.`, + 'Please, install v3.x of the package or use the `AdapterDateFns` instead.', + ].join('\n'), + ); + } + if (!longFormatters) { + throw new Error( + 'MUI: The minimum supported `date-fns` package version compatible with this adapter is `3.2.x`.', + ); + } + super({ locale: locale ?? enUS, formats, longFormatters }); + } + + public parse = (value: string, format: string) => { + if (value === '') { + return null; + } + + return dateFnsParse(value, format, new Date(), { locale: this.locale }); + }; + + public isValid = (value: Date | null) => { + if (value == null) { + return false; + } + + return isValid(value); + }; + + public format = (value: Date, formatKey: keyof AdapterFormats) => { + return this.formatByString(value, this.formats[formatKey]); + }; + + public formatByString = (value: Date, formatString: string) => { + return dateFnsFormat(value, formatString, { locale: this.locale }); + }; + + public isEqual = (value: Date | null, comparing: Date | null) => { + if (value === null && comparing === null) { + return true; + } + + if (value === null || comparing === null) { + return false; + } + + return isEqual(value, comparing); + }; + + public isSameYear = (value: Date, comparing: Date) => { + return isSameYear(value, comparing); + }; + + public isSameMonth = (value: Date, comparing: Date) => { + return isSameMonth(value, comparing); + }; + + public isSameDay = (value: Date, comparing: Date) => { + return isSameDay(value, comparing); + }; + + public isSameHour = (value: Date, comparing: Date) => { + return isSameHour(value, comparing); + }; + + public isAfter = (value: Date, comparing: Date) => { + return isAfter(value, comparing); + }; + + public isAfterYear = (value: Date, comparing: Date) => { + return isAfter(value, endOfYear(comparing)); + }; + + public isAfterDay = (value: Date, comparing: Date) => { + return isAfter(value, endOfDay(comparing)); + }; + + public isBefore = (value: Date, comparing: Date) => { + return isBefore(value, comparing); + }; + + public isBeforeYear = (value: Date, comparing: Date) => { + return isBefore(value, this.startOfYear(comparing)); + }; + + public isBeforeDay = (value: Date, comparing: Date) => { + return isBefore(value, this.startOfDay(comparing)); + }; + + public isWithinRange = (value: Date, [start, end]: [Date, Date]) => { + return isWithinInterval(value, { start, end }); + }; + + public startOfYear = (value: Date) => { + return startOfYear(value); + }; + + public startOfMonth = (value: Date) => { + return startOfMonth(value); + }; + + public startOfWeek = (value: Date) => { + return startOfWeek(value, { locale: this.locale }); + }; + + public startOfDay = (value: Date) => { + return startOfDay(value); + }; + + public endOfYear = (value: Date) => { + return endOfYear(value); + }; + + public endOfMonth = (value: Date) => { + return endOfMonth(value); + }; + + public endOfWeek = (value: Date) => { + return endOfWeek(value, { locale: this.locale }); + }; + + public endOfDay = (value: Date) => { + return endOfDay(value); + }; + + public addYears = (value: Date, amount: number) => { + return addYears(value, amount); + }; + + public addMonths = (value: Date, amount: number) => { + return addMonths(value, amount); + }; + + public addWeeks = (value: Date, amount: number) => { + return addWeeks(value, amount); + }; + + public addDays = (value: Date, amount: number) => { + return addDays(value, amount); + }; + + public addHours = (value: Date, amount: number) => { + return addHours(value, amount); + }; + + public addMinutes = (value: Date, amount: number) => { + return addMinutes(value, amount); + }; + + public addSeconds = (value: Date, amount: number) => { + return addSeconds(value, amount); + }; + + public getYear = (value: Date) => { + return getYear(value); + }; + + public getMonth = (value: Date) => { + return getMonth(value); + }; + + public getDate = (value: Date) => { + return getDate(value); + }; + + public getHours = (value: Date) => { + return getHours(value); + }; + + public getMinutes = (value: Date) => { + return getMinutes(value); + }; + + public getSeconds = (value: Date) => { + return getSeconds(value); + }; + + public getMilliseconds = (value: Date) => { + return getMilliseconds(value); + }; + + public setYear = (value: Date, year: number) => { + return setYear(value, year); + }; + + public setMonth = (value: Date, month: number) => { + return setMonth(value, month); + }; + + public setDate = (value: Date, date: number) => { + return setDate(value, date); + }; + + public setHours = (value: Date, hours: number) => { + return setHours(value, hours); + }; + + public setMinutes = (value: Date, minutes: number) => { + return setMinutes(value, minutes); + }; + + public setSeconds = (value: Date, seconds: number) => { + return setSeconds(value, seconds); + }; + + public setMilliseconds = (value: Date, milliseconds: number) => { + return setMilliseconds(value, milliseconds); + }; + + public getDaysInMonth = (value: Date) => { + return getDaysInMonth(value); + }; + + public getWeekArray = (value: Date) => { + const start = this.startOfWeek(this.startOfMonth(value)); + const end = this.endOfWeek(this.endOfMonth(value)); + + let count = 0; + let current = start; + const nestedWeeks: Date[][] = []; + + while (this.isBefore(current, end)) { + const weekNumber = Math.floor(count / 7); + nestedWeeks[weekNumber] = nestedWeeks[weekNumber] || []; + nestedWeeks[weekNumber].push(current); + + current = this.addDays(current, 1); + count += 1; + } + + return nestedWeeks; + }; + + public getWeekNumber = (value: Date) => { + return getWeek(value, { locale: this.locale }); + }; + + public getYearRange = ([start, end]: [Date, Date]) => { + const startDate = this.startOfYear(start); + const endDate = this.endOfYear(end); + const years: Date[] = []; + + let current = startDate; + while (this.isBefore(current, endDate)) { + years.push(current); + current = this.addYears(current, 1); + } + + return years; + }; +} diff --git a/packages/x-date-pickers/src/AdapterDateFnsV3/index.ts b/packages/x-date-pickers/src/AdapterDateFnsV3/index.ts new file mode 100644 index 0000000000000..ebecde913f306 --- /dev/null +++ b/packages/x-date-pickers/src/AdapterDateFnsV3/index.ts @@ -0,0 +1 @@ +export { AdapterDateFns } from './AdapterDateFnsV3'; diff --git a/packages/x-date-pickers/src/internals/models/helpers.ts b/packages/x-date-pickers/src/internals/models/helpers.ts index c90beb398811e..18232239ffb04 100644 --- a/packages/x-date-pickers/src/internals/models/helpers.ts +++ b/packages/x-date-pickers/src/internals/models/helpers.ts @@ -10,6 +10,8 @@ export type ExtendMui = Omit< export type MakeOptional = Omit & Partial>; +export type MakeRequired = Omit & Required>; + export type DefaultizedProps< P extends {}, RequiredProps extends keyof P, diff --git a/patches/babel-plugin-replace-imports+1.0.2.patch b/patches/babel-plugin-replace-imports+1.0.2.patch new file mode 100644 index 0000000000000..1c47ea8f960d3 --- /dev/null +++ b/patches/babel-plugin-replace-imports+1.0.2.patch @@ -0,0 +1,9 @@ +diff --git a/node_modules/babel-plugin-replace-imports/lib/index.js b/node_modules/babel-plugin-replace-imports/lib/index.js +index 8da63ad..9712bcc 100644 +--- a/node_modules/babel-plugin-replace-imports/lib/index.js ++++ b/node_modules/babel-plugin-replace-imports/lib/index.js +@@ -1 +1 @@ +-'use strict';Object.defineProperty(exports,'__esModule',{value:!0}),exports.optionLabels=void 0,exports.getErrorMessage=getErrorMessage;var _lodash=require('lodash.isempty'),_lodash2=_interopRequireDefault(_lodash),_lodash3=require('lodash.isstring'),_lodash4=_interopRequireDefault(_lodash3),_lodash5=require('lodash.isregexp'),_lodash6=_interopRequireDefault(_lodash5),_lodash7=require('lodash.isobject'),_lodash8=_interopRequireDefault(_lodash7),_lodash9=require('lodash.isfunction'),_lodash10=_interopRequireDefault(_lodash9);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}var PLUGIN='babel-plugin-replace-imports',ERRORS={0:'options are required.',1:'option is required.',2:'option must be a RegExp.',3:'option must be a String or a Function',4:'options item must be an Object.'},optionLabels=exports.optionLabels={test:'test',replacer:'replacer'};function getErrorMessage(code,text){var msg=((text?'\xAB'+text+'\xBB':'')+' '+ERRORS[code]).trim();return'\n'+PLUGIN+': '+msg}function init(_ref){function throwError(code,text){var msg=getErrorMessage(code,text);throw new Error(msg)}function getOption(option){return(!(0,_lodash8.default)(option)||(0,_lodash6.default)(option)||Array.isArray(option))&&throwError(4),option}function getTestOption(option){return!(0,_lodash6.default)(option)&&(0,_lodash2.default)(option)&&throwError(1,optionLabels.test),(0,_lodash6.default)(option)||throwError(2,optionLabels.test),option}function getReplacerListOption(option){return(0,_lodash10.default)(option)?[option]:((0,_lodash2.default)(option)&&throwError(1,optionLabels.replacer),Array.isArray(option)?option:[option])}function getReplacerOption(option){return(0,_lodash4.default)(option)||(0,_lodash10.default)(option)||throwError(3,optionLabels.replacer),option}var types=_ref.types;return{visitor:{ImportDeclaration:function ImportDeclaration(path,_ref2){var opts=_ref2.opts;if(!path.node.__processed){(0,_lodash2.default)(opts)&&throwError(0);var source=path.node.source.value,transforms=[],options=opts;Array.isArray(options)||(options=[opts]);for(var _ret,_loop=function(i){var opt=getOption(options[i]),regex=getTestOption(opt[optionLabels.test]);if(regex.test(source)){var replacerList=getReplacerListOption(opt[optionLabels.replacer]);return replacerList.forEach(function(replacer){var repl=getReplacerOption(replacer),importDeclaration=types.importDeclaration(path.node.specifiers,types.stringLiteral(source.replace(regex,repl)));importDeclaration.__processed=!0,transforms.push(importDeclaration)}),'break'}},i=0;i