From dfa11605834ff394f3d971ada390e86a2e78597e Mon Sep 17 00:00:00 2001 From: Flavien DELANGLE Date: Wed, 20 Mar 2024 08:42:42 +0100 Subject: [PATCH] [fields] Support format without separator (#12489) --- .../src/AdapterLuxon/AdapterLuxon.ts | 14 ++-- .../DateField/tests/format.DateField.test.tsx | 9 +++ .../hooks/useField/buildSectionsFromFormat.ts | 66 ++++++++++--------- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts index 6dba4e0d75201..066a0ab572b6e 100644 --- a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts +++ b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts @@ -214,6 +214,10 @@ export class AdapterLuxon implements MuiPickersAdapter { // Extract escaped section to avoid extending them const catchEscapedSectionsRegexp = /''|'(''|[^'])+('|$)|[^']*/g; + // This RegExp tests if a string is only mad of supported tokens + const validTokens = [...Object.keys(this.formatTokenMap), 'yyyyy']; + const isWordComposedOfTokens = new RegExp(`^(${validTokens.join('|')})+$`); + // Extract words to test if they are a token or a word to escape. const catchWordsRegexp = /(?:^|[^a-z])([a-z]+)(?:[^a-z]|$)|([a-z]+)/gi; return ( @@ -225,12 +229,14 @@ export class AdapterLuxon implements MuiPickersAdapter { return token; } const expandedToken = DateTime.expandFormat(token, { locale: this.locale }); - return expandedToken.replace(catchWordsRegexp, (correspondance, g1, g2) => { + + return expandedToken.replace(catchWordsRegexp, (substring, g1, g2) => { const word = g1 || g2; // words are either in group 1 or group 2 - if (word === 'yyyyy' || formatTokenMap[word] !== undefined) { - return correspondance; + + if (isWordComposedOfTokens.test(word)) { + return substring; } - return `'${correspondance}'`; + return `'${substring}'`; }); }) .join('') diff --git a/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx b/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx index 51e71600acee8..f680275405ce3 100644 --- a/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx +++ b/packages/x-date-pickers/src/DateField/tests/format.DateField.test.tsx @@ -160,6 +160,15 @@ describeAdapters(' - Format', DateField, ({ adapter, renderWithProp expectFieldPlaceholderV6(input, 'Escaped Escaped'); }); + it('should support format without separators', () => { + const v7Response = renderWithProps({ + enableAccessibleFieldDOMStructure: true, + format: `${adapter.formats.dayOfMonth}${adapter.formats.monthShort}`, + }); + + expectFieldValueV7(v7Response.getSectionsContainer(), 'DDMMMM'); + }); + it('should add spaces around `/` when `formatDensity = "spacious"`', () => { // Test with v7 input const v7Response = renderWithProps({ diff --git a/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts b/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts index 5e2732cb0b275..63e05d31e7765 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts @@ -202,30 +202,44 @@ const buildSections = ( const sections: FieldSection[] = []; let startSeparator: string = ''; - // This RegExp test if the beginning of a string corresponds to a supported token - const isTokenStartRegExp = new RegExp( - `^(${Object.keys(utils.formatTokenMap) - .sort((a, b) => b.length - a.length) // Sort to put longest word first - .join('|')})`, - 'g', // used to get access to lastIndex state - ); + // This RegExp tests if the beginning of a string corresponds to a supported token + const validTokens = Object.keys(utils.formatTokenMap).sort((a, b) => b.length - a.length); // Sort to put longest word first - let currentTokenValue = ''; + const regExpFirstWordInFormat = /^([a-zA-Z]+)/; + const regExpWordOnlyComposedOfTokens = new RegExp(`^(${validTokens.join('|')})*$`); + const regExpFirstTokenInWord = new RegExp(`^(${validTokens.join('|')})`); - for (let i = 0; i < expandedFormat.length; i += 1) { - const escapedPartOfCurrentChar = escapedParts.find( - (escapeIndex) => escapeIndex.start <= i && escapeIndex.end >= i, - ); + const getEscapedPartOfCurrentChar = (i: number) => + escapedParts.find((escapeIndex) => escapeIndex.start <= i && escapeIndex.end >= i); - const char = expandedFormat[i]; + let i = 0; + while (i < expandedFormat.length) { + const escapedPartOfCurrentChar = getEscapedPartOfCurrentChar(i); const isEscapedChar = escapedPartOfCurrentChar != null; - const potentialToken = `${currentTokenValue}${expandedFormat.slice(i)}`; - const regExpMatch = isTokenStartRegExp.test(potentialToken); + const firstWordInFormat = regExpFirstWordInFormat.exec(expandedFormat.slice(i))?.[1]; + + // The first word in the format is only composed of tokens. + // We extract those tokens to create a new sections. + if ( + !isEscapedChar && + firstWordInFormat != null && + regExpWordOnlyComposedOfTokens.test(firstWordInFormat) + ) { + let word = firstWordInFormat; + while (word.length > 0) { + const firstWord = regExpFirstTokenInWord.exec(word)![1]; + word = word.slice(firstWord.length); + sections.push(createSection({ ...params, now, token: firstWord, startSeparator })); + startSeparator = ''; + } + + i += firstWordInFormat.length; + } + // The remaining format does not start with a token, + // We take the first character and add it to the current section's end separator. + else { + const char = expandedFormat[i]; - if (!isEscapedChar && char.match(/([A-Za-z]+)/) && regExpMatch) { - currentTokenValue = potentialToken.slice(0, isTokenStartRegExp.lastIndex); - i += isTokenStartRegExp.lastIndex - 1; - } else { // If we are on the opening or closing character of an escaped part of the format, // Then we ignore this character. const isEscapeBoundary = @@ -233,25 +247,15 @@ const buildSections = ( escapedPartOfCurrentChar?.end === i; if (!isEscapeBoundary) { - if (currentTokenValue !== '') { - sections.push( - createSection({ ...params, now, token: currentTokenValue, startSeparator }), - ); - currentTokenValue = ''; - } - if (sections.length === 0) { startSeparator += char; } else { - startSeparator = ''; sections[sections.length - 1].endSeparator += char; } } - } - } - if (currentTokenValue !== '') { - sections.push(createSection({ ...params, now, token: currentTokenValue, startSeparator })); + i += 1; + } } if (sections.length === 0 && startSeparator.length > 0) {