Skip to content

Commit

Permalink
[pickers] Infer mask from inputFormat (#5060)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfauquette authored Jun 8, 2022
1 parent 8623b45 commit a05b853
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 50 deletions.
8 changes: 0 additions & 8 deletions docs/data/date-pickers/date-picker/LocalizedDatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -50,7 +43,6 @@ export default function LocalizedDatePicker() {
))}
</ToggleButtonGroup>
<DatePicker
mask={maskMap[locale]}
value={value}
onChange={(newValue) => setValue(newValue)}
renderInput={(params) => <TextField {...params} />}
Expand Down
10 changes: 1 addition & 9 deletions docs/data/date-pickers/date-picker/LocalizedDatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,8 @@ const localeMap = {
de: deLocale,
};

const maskMap = {
fr: '__/__/____',
en: '__/__/____',
ru: '__.__.____',
de: '__.__.____',
};

export default function LocalizedDatePicker() {
const [locale, setLocale] = React.useState<keyof typeof maskMap>('ru');
const [locale, setLocale] = React.useState<keyof typeof localeMap>('ru');
const [value, setValue] = React.useState<Date | null>(new Date());

const selectLocale = (newLocale: any) => {
Expand All @@ -50,7 +43,6 @@ export default function LocalizedDatePicker() {
))}
</ToggleButtonGroup>
<DatePicker
mask={maskMap[locale]}
value={value}
onChange={(newValue) => setValue(newValue)}
renderInput={(params) => <TextField {...params} />}
Expand Down
6 changes: 1 addition & 5 deletions packages/x-date-pickers-pro/src/DateRangePicker/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,7 @@ export function useDateRangePickerDefaultizedProps<
name: string,
): DefaultizedProps<Props> &
Required<
Pick<
BaseDateRangePickerProps<TInputDate, TDate>,
'calendars' | 'mask' | 'startText' | 'endText'
>
Pick<BaseDateRangePickerProps<TInputDate, TDate>, 'calendars' | 'startText' | 'endText'>
> {
const utils = useUtils<TDate>();
const defaultDates = useDefaultDates();
Expand All @@ -88,7 +85,6 @@ export function useDateRangePickerDefaultizedProps<

return {
calendars: 2,
mask: '__/__/____',
inputFormat: utils.formats.keyboardDate,
minDate: defaultDates.minDate,
maxDate: defaultDates.maxDate,
Expand Down
2 changes: 0 additions & 2 deletions packages/x-date-pickers/src/DatePicker/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const getFormatAndMaskByViews = <TDate>(
): { disableMaskedInput?: boolean; inputFormat: string; mask?: string } => {
if (isYearOnlyView(views)) {
return {
mask: '____',
inputFormat: utils.formats.year,
};
}
Expand All @@ -78,7 +77,6 @@ const getFormatAndMaskByViews = <TDate>(
}

return {
mask: '__/__/____',
inputFormat: utils.formats.keyboardDate,
};
};
Expand Down
1 change: 0 additions & 1 deletion packages/x-date-pickers/src/DateTimePicker/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,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,
Expand Down
1 change: 0 additions & 1 deletion packages/x-date-pickers/src/TimePicker/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export function useTimePickerDefaultizedProps<
openTo: 'hours',
views: ['hours', 'minutes'],
acceptRegex: ampm ? /[\dapAP]/gi : /\d/gi,
mask: ampm ? '__:__ _m' : '__:__',
disableMaskedInput: false,
getOpenDialogAriaText,
inputFormat: ampm ? utils.formats.fullTime12h : utils.formats.fullTime24h,
Expand Down
24 changes: 18 additions & 6 deletions packages/x-date-pickers/src/internals/hooks/useMaskedInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
maskedDateFormatter,
getDisplayDate,
checkMaskIsValidForCurrentFormat,
getMaskFromCurrentFormat,
} from '../utils/text-field-helper';

type MaskedInputProps<TInputDate, TDate> = Omit<
Expand Down Expand Up @@ -41,19 +42,30 @@ export const useMaskedInput = <TInputDate, TDate>({

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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,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.');
}
});
});
Expand Down
68 changes: 56 additions & 12 deletions packages/x-date-pickers/src/internals/utils/text-field-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,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<any>,
) {
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<any>,
) {
if (!mask) {
return false;
}

const formattedDateWith1Digit = utils.formatByString(
utils.date(staticDateWith1DigitTokens)!,
format,
Expand All @@ -47,35 +90,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'));
}
}

Expand Down

0 comments on commit a05b853

Please sign in to comment.