diff --git a/docs/data/date-pickers/date-picker/LocalizedDatePicker.js b/docs/data/date-pickers/date-picker/LocalizedDatePicker.js index 51890786072e..14a23fc638a1 100644 --- a/docs/data/date-pickers/date-picker/LocalizedDatePicker.js +++ b/docs/data/date-pickers/date-picker/LocalizedDatePicker.js @@ -17,13 +17,6 @@ const localeMap = { de: deLocale, }; -const maskMap = { - fr: '__/__/____', - en: '__/__/____', - ru: '__.__.____', - de: '__.__.____', -}; - export default function LocalizedDatePicker() { const [locale, setLocale] = React.useState('ru'); const [value, setValue] = React.useState(new Date()); @@ -50,7 +43,6 @@ export default function LocalizedDatePicker() { ))} setValue(newValue)} renderInput={(params) => } diff --git a/docs/data/date-pickers/date-picker/LocalizedDatePicker.tsx b/docs/data/date-pickers/date-picker/LocalizedDatePicker.tsx index 798bb16a1989..006c3d29896b 100644 --- a/docs/data/date-pickers/date-picker/LocalizedDatePicker.tsx +++ b/docs/data/date-pickers/date-picker/LocalizedDatePicker.tsx @@ -17,15 +17,8 @@ const localeMap = { de: deLocale, }; -const maskMap = { - fr: '__/__/____', - en: '__/__/____', - ru: '__.__.____', - de: '__.__.____', -}; - export default function LocalizedDatePicker() { - const [locale, setLocale] = React.useState('ru'); + const [locale, setLocale] = React.useState('ru'); const [value, setValue] = React.useState(new Date()); const selectLocale = (newLocale: any) => { @@ -50,7 +43,6 @@ export default function LocalizedDatePicker() { ))} setValue(newValue)} renderInput={(params) => } diff --git a/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts b/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts index d03d1ce2d155..54aff0e15ffe 100644 --- a/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts +++ b/packages/x-date-pickers-pro/src/DateRangePicker/shared.ts @@ -75,10 +75,7 @@ export function useDateRangePickerDefaultizedProps< name: string, ): DefaultizedProps & Required< - Pick< - BaseDateRangePickerProps, - 'calendars' | 'mask' | 'startText' | 'endText' - > + Pick, 'calendars' | 'startText' | 'endText'> > { const utils = useUtils(); const defaultDates = useDefaultDates(); @@ -102,7 +99,6 @@ export function useDateRangePickerDefaultizedProps< return { calendars: 2, - mask: '__/__/____', inputFormat: utils.formats.keyboardDate, minDate: defaultDates.minDate, maxDate: defaultDates.maxDate, diff --git a/packages/x-date-pickers-pro/tsconfig.json b/packages/x-date-pickers-pro/tsconfig.json index d8409ccca8f7..092d1e97918b 100644 --- a/packages/x-date-pickers-pro/tsconfig.json +++ b/packages/x-date-pickers-pro/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig", "compilerOptions": { + // @date-io libraries produce duplicate type `DateType` + "skipLibCheck": true, "types": ["react", "mocha", "node"] }, "include": ["src/**/*", "../../node_modules/@mui/monorepo/test/utils/initMatchers.ts"] diff --git a/packages/x-date-pickers/src/DatePicker/shared.ts b/packages/x-date-pickers/src/DatePicker/shared.ts index a7edd1e400c8..c91fe305ec72 100644 --- a/packages/x-date-pickers/src/DatePicker/shared.ts +++ b/packages/x-date-pickers/src/DatePicker/shared.ts @@ -81,7 +81,6 @@ const getFormatAndMaskByViews = ( ): { disableMaskedInput?: boolean; inputFormat: string; mask?: string } => { if (isYearOnlyView(views)) { return { - mask: '____', inputFormat: utils.formats.year, }; } @@ -94,7 +93,6 @@ const getFormatAndMaskByViews = ( } return { - mask: '__/__/____', inputFormat: utils.formats.keyboardDate, }; }; diff --git a/packages/x-date-pickers/src/DateTimePicker/shared.ts b/packages/x-date-pickers/src/DateTimePicker/shared.ts index 485025667140..d637e492ba76 100644 --- a/packages/x-date-pickers/src/DateTimePicker/shared.ts +++ b/packages/x-date-pickers/src/DateTimePicker/shared.ts @@ -125,7 +125,6 @@ export function useDateTimePickerDefaultizedProps< openTo: 'day', views: ['year', 'day', 'hours', 'minutes'], ampmInClock: true, - mask: ampm ? '__/__/____ __:__ _m' : '__/__/____ __:__', acceptRegex: ampm ? /[\dap]/gi : /\d/gi, disableMaskedInput: false, inputFormat: ampm ? utils.formats.keyboardDateTime12h : utils.formats.keyboardDateTime24h, diff --git a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePickerKeyboard.test.tsx b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePickerKeyboard.test.tsx index bd2c926787c7..79db58128774 100644 --- a/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePickerKeyboard.test.tsx +++ b/packages/x-date-pickers/src/DesktopDatePicker/DesktopDatePickerKeyboard.test.tsx @@ -165,7 +165,7 @@ describe(' keyboard interactions', () => { const onErrorMock = spy(); // we are running validation on value change function DatePickerInput() { - const [date, setDate] = React.useState(null); + const [date, setDate] = React.useState(null); return ( ', () => { const { render } = createPickerRenderer({ clock: 'fake', - clockConfig: adapterToUse.date('2018-01-01T00:00:00.000').getTime(), + clockConfig: new Date('2018-01-01T00:00:00.000'), }); ['readOnly', 'disabled'].forEach((prop) => { diff --git a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.test.tsx b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.test.tsx index 7f93c05598ca..d608ae1d1fc5 100644 --- a/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.test.tsx +++ b/packages/x-date-pickers/src/MobileDateTimePicker/MobileDateTimePicker.test.tsx @@ -21,7 +21,7 @@ const WrappedMobileDateTimePicker = withPickerControls(MobileDateTimePicker)({ describe('', () => { const { render } = createPickerRenderer({ clock: 'fake', - clockConfig: adapterToUse.date('2018-01-01T00:00:00.000').getTime(), + clockConfig: new Date('2018-01-01T00:00:00.000'), }); it('prop: open – overrides open state', () => { diff --git a/packages/x-date-pickers/src/TimePicker/shared.ts b/packages/x-date-pickers/src/TimePicker/shared.ts index ca6c4ddcce84..31d74b7068d1 100644 --- a/packages/x-date-pickers/src/TimePicker/shared.ts +++ b/packages/x-date-pickers/src/TimePicker/shared.ts @@ -75,7 +75,6 @@ export function useTimePickerDefaultizedProps< openTo: 'hours', views: ['hours', 'minutes'], acceptRegex: ampm ? /[\dapAP]/gi : /\d/gi, - mask: ampm ? '__:__ _m' : '__:__', disableMaskedInput: false, getOpenDialogAriaText: getTextFieldAriaText, inputFormat: ampm ? utils.formats.fullTime12h : utils.formats.fullTime24h, diff --git a/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx b/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx index f60bee59a492..9b1a9c8f28d7 100644 --- a/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx +++ b/packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx @@ -7,6 +7,7 @@ import { maskedDateFormatter, getDisplayDate, checkMaskIsValidForCurrentFormat, + getMaskFromCurrentFormat, } from '../utils/text-field-helper'; type MaskedInputProps = Omit< @@ -43,19 +44,30 @@ export const useMaskedInput = ({ const formatHelperText = utils.getFormatHelperText(inputFormat); - const shouldUseMaskedInput = React.useMemo(() => { + const { shouldUseMaskedInput, maskToUse } = React.useMemo(() => { // formatting of dates is a quite slow thing, so do not make useless .format calls - if (!mask || disableMaskedInput) { - return false; + if (disableMaskedInput) { + return { shouldUseMaskedInput: false, maskToUse: '' }; } + const computedMaskToUse = getMaskFromCurrentFormat(mask, inputFormat, acceptRegex, utils); - return checkMaskIsValidForCurrentFormat(mask, inputFormat, acceptRegex, utils); + return { + shouldUseMaskedInput: checkMaskIsValidForCurrentFormat( + computedMaskToUse, + inputFormat, + acceptRegex, + utils, + ), + maskToUse: computedMaskToUse, + }; }, [acceptRegex, disableMaskedInput, inputFormat, mask, utils]); const formatter = React.useMemo( () => - shouldUseMaskedInput && mask ? maskedDateFormatter(mask, acceptRegex) : (st: string) => st, - [acceptRegex, mask, shouldUseMaskedInput], + shouldUseMaskedInput && maskToUse + ? maskedDateFormatter(maskToUse, acceptRegex) + : (st: string) => st, + [acceptRegex, maskToUse, shouldUseMaskedInput], ); // TODO: Implement with controlled vs uncontrolled `rawValue` diff --git a/packages/x-date-pickers/src/internals/utils/text-field-helper.test.ts b/packages/x-date-pickers/src/internals/utils/text-field-helper.test.ts index b50d51aeefb2..51b3dc3ccce1 100644 --- a/packages/x-date-pickers/src/internals/utils/text-field-helper.test.ts +++ b/packages/x-date-pickers/src/internals/utils/text-field-helper.test.ts @@ -26,18 +26,42 @@ describe('text-field-helper', () => { }); [ - { mask: '__.__.____', format: adapterToUse.formats.keyboardDate, isValid: false }, - { mask: '__/__/____', format: adapterToUse.formats.keyboardDate, isValid: true }, - { mask: '__:__ _m', format: adapterToUse.formats.fullTime, isValid: false }, - { mask: '__/__/____ __:__ _m', format: adapterToUse.formats.keyboardDateTime, isValid: false }, - { mask: '__/__/____ __:__', format: adapterToUse.formats.keyboardDateTime24h, isValid: true }, - { mask: '__/__/____', format: 'MM/dd/yyyy', isValid: true }, - { mask: '__/__/____', format: 'MMMM yyyy', isValid: false }, + // Time picker + // - with ampm = true + { mask: '__:__ _m', format: adapterToUse.formats.fullTime12h, isValid: true }, + // - with ampm=false + { mask: '__:__', format: adapterToUse.formats.fullTime24h, isValid: true }, + // Date Picker + { + mask: '__/__/____', + format: adapterToUse.formats.keyboardDate, + isValid: true, + }, + // - with year only + { + mask: '____', + format: adapterToUse.formats.year, + isValid: true, + }, + // DateTimePicker + // - with ampm=true { mask: '__/__/____ __:__ _m', format: adapterToUse.formats.keyboardDateTime12h, isValid: true, }, + // - with ampm=false + { + mask: '__/__/____ __:__', + format: adapterToUse.formats.keyboardDateTime24h, + isValid: true, + }, + // Test rejections + { mask: '__.__.____', format: adapterToUse.formats.keyboardDate, isValid: false }, + { mask: '__:__ _m', format: adapterToUse.formats.fullTime, isValid: false }, + { mask: '__/__/____ __:__ _m', format: adapterToUse.formats.keyboardDateTime, isValid: false }, + { mask: '__/__/____', format: 'MM/dd/yyyy', isValid: adapterToUse.lib === 'date-fns' }, // only pass with date-fns + { mask: '__/__/____', format: 'MMMM yyyy', isValid: false }, ].forEach(({ mask, format, isValid }, index) => { it(`checkMaskIsValidFormat returns ${isValid} for mask #${index} '${mask}' and format ${format}`, () => { const runMaskValidation = () => @@ -46,12 +70,7 @@ describe('text-field-helper', () => { if (isValid) { expect(runMaskValidation()).to.be.equal(true); } else { - expect(runMaskValidation).toWarnDev( - [ - `The mask "${mask}" you passed is not valid for the format used ${format}.`, - `Falling down to uncontrolled no-mask input.`, - ].join('\n'), - ); + expect(runMaskValidation).toWarnDev('Falling down to uncontrolled no-mask input.'); } }); }); diff --git a/packages/x-date-pickers/src/internals/utils/text-field-helper.ts b/packages/x-date-pickers/src/internals/utils/text-field-helper.ts index f260a203fe5a..bb52d604df2e 100644 --- a/packages/x-date-pickers/src/internals/utils/text-field-helper.ts +++ b/packages/x-date-pickers/src/internals/utils/text-field-helper.ts @@ -39,12 +39,55 @@ const MASK_USER_INPUT_SYMBOL = '_'; const staticDateWith2DigitTokens = '2019-11-21T22:30:00.000'; const staticDateWith1DigitTokens = '2019-01-01T09:00:00.000'; +export function getMaskFromCurrentFormat( + mask: string | undefined, + format: string, + acceptRegex: RegExp, + utils: MuiPickersAdapter, +) { + if (mask) { + return mask; + } + + const formattedDateWith1Digit = utils.formatByString( + utils.date(staticDateWith1DigitTokens)!, + format, + ); + const inferredFormatPatternWith1Digits = formattedDateWith1Digit.replace( + acceptRegex, + MASK_USER_INPUT_SYMBOL, + ); + + const inferredFormatPatternWith2Digits = utils + .formatByString(utils.date(staticDateWith2DigitTokens)!, format) + .replace(acceptRegex, '_'); + + if (inferredFormatPatternWith1Digits === inferredFormatPatternWith2Digits) { + return inferredFormatPatternWith1Digits; + } + + if (process.env.NODE_ENV !== 'production') { + console.warn( + [ + `Mask does not support numbers with variable length such as 'M'.`, + `Either use numbers with fix length or disable mask feature with 'disableMaskedInput' prop`, + `Falling down to uncontrolled no-mask input.`, + ].join('\n'), + ); + } + return ''; +} + export function checkMaskIsValidForCurrentFormat( mask: string, format: string, acceptRegex: RegExp, utils: MuiPickersAdapter, ) { + if (!mask) { + return false; + } + const formattedDateWith1Digit = utils.formatByString( utils.date(staticDateWith1DigitTokens)!, format, @@ -59,35 +102,36 @@ export function checkMaskIsValidForCurrentFormat( .replace(acceptRegex, '_'); const isMaskValid = - inferredFormatPatternWith2Digits === mask && inferredFormatPatternWith1Digits === mask; + inferredFormatPatternWith2Digits === inferredFormatPatternWith1Digits && + mask === inferredFormatPatternWith2Digits; if (!isMaskValid && utils.lib !== 'luxon' && process.env.NODE_ENV !== 'production') { - const defaultWarning = [ - `The mask "${mask}" you passed is not valid for the format used ${format}.`, - `Falling down to uncontrolled no-mask input.`, - ]; - if (format.includes('MMM')) { console.warn( [ - ...defaultWarning, `Mask does not support literals such as 'MMM'.`, `Either use numbers with fix length or disable mask feature with 'disableMaskedInput' prop`, + `Falling down to uncontrolled no-mask input.`, ].join('\n'), ); } else if ( - inferredFormatPatternWith2Digits !== mask && - inferredFormatPatternWith1Digits === mask + inferredFormatPatternWith2Digits && + inferredFormatPatternWith2Digits !== inferredFormatPatternWith1Digits ) { console.warn( [ - ...defaultWarning, `Mask does not support numbers with variable length such as 'M'.`, `Either use numbers with fix length or disable mask feature with 'disableMaskedInput' prop`, + `Falling down to uncontrolled no-mask input.`, + ].join('\n'), + ); + } else if (mask) { + console.warn( + [ + `The mask "${mask}" you passed is not valid for the format used ${format}.`, + `Falling down to uncontrolled no-mask input.`, ].join('\n'), ); - } else { - console.warn(defaultWarning.join('\n')); } } diff --git a/test/utils/pickers-utils.tsx b/test/utils/pickers-utils.tsx index 829da051062d..85c9aac437a7 100644 --- a/test/utils/pickers-utils.tsx +++ b/test/utils/pickers-utils.tsx @@ -1,34 +1,48 @@ import * as React from 'react'; -import { parseISO } from 'date-fns'; import { createRenderer, screen, RenderOptions, userEvent } from '@mui/monorepo/test/utils'; import { CreateRendererOptions } from '@mui/monorepo/test/utils/createRenderer'; import { TransitionProps } from '@mui/material/transitions'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; +import { AdapterMoment } from '@mui/x-date-pickers/AdapterMoment'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -// TODO make possible to pass here any utils using cli -/** - * Wrapper around `@date-io/date-fns` that resolves https://github.com/dmtrKovalenko/date-io/issues/479. - * We're not using `adapter.date` in the implementation which means the implementation is safe. - * But we do use it in tests where usage of ISO dates without timezone is problematic - */ -export class AdapterClassToUse extends AdapterDateFns { - // Inlined AdapterDateFns#date which is not an instance method but instance property - // eslint-disable-next-line class-methods-use-this - date = (value?: any): Date => { - if (typeof value === 'string') { - return parseISO(value); - } - if (typeof value === 'undefined') { - return new Date(); - } - if (value === null) { - // @ts-expect-error AdapterDateFns#date says it returns NotNullable but that's not true - return null; - } - return new Date(value); - }; +const availableAdapters = ['date-fns', 'day-js', 'luxon', 'moment']; +let adapter = 'date-fns'; + +// Check if we are in unit tests +if (/jsdom/.test(window.navigator.userAgent)) { + // Add parameter `--date-adapter luxon` to use AdapterLuxon for running tests + // adapter available : date-fns (default one), day-js, luxon, moment + const args = process.argv.slice(2); + const flagIndex = args.findIndex((element) => element === '--date-adapter'); + const potentialAdapter = flagIndex + 1 < args.length ? args[flagIndex + 1] : null; + if (potentialAdapter && availableAdapters.includes(potentialAdapter)) { + adapter = potentialAdapter; + } } + +let AdapterClassToExtend; +switch (adapter) { + case 'day-js': + AdapterClassToExtend = AdapterDayjs; + break; + + case 'luxon': + AdapterClassToExtend = AdapterLuxon; + break; + + case 'moment': + AdapterClassToExtend = AdapterMoment; + break; + + default: + AdapterClassToExtend = AdapterDateFns; + break; +} +export class AdapterClassToUse extends AdapterClassToExtend {} + export const adapterToUse = new AdapterClassToUse(); export const FakeTransitionComponent = React.forwardRef( @@ -43,8 +57,8 @@ export const FakeTransitionComponent = React.forwardRef import('enzyme').ReactWrapper) { @@ -58,9 +72,14 @@ export function createPickerRenderer({ }: CreatePickerRendererOptions = {}) { const { clock, render: clientRender } = createRenderer(createRendererOptions); + let adapterLocale = adapterToUse.lib === 'date-fns' ? locale : locale?.code; + + if (typeof adapterLocale === 'string' && adapterLocale.length > 2) { + adapterLocale = adapterLocale.slice(0, 2); + } function Wrapper({ children }: { children?: React.ReactNode }) { return ( - + {children} );