From b078cb3db0a8886d509815a85e596b0070b2910a Mon Sep 17 00:00:00 2001 From: delangle Date: Tue, 19 Mar 2024 10:39:15 +0100 Subject: [PATCH 1/4] [fields] Support format without separator --- .../src/AdapterLuxon/AdapterLuxon.ts | 14 ++++-- .../DateField/tests/format.DateField.test.tsx | 11 +++- .../hooks/useField/buildSectionsFromFormat.ts | 50 ++++++++----------- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts index 6dba4e0d75201..da7b2819add3a 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 isTokenStartRegExpStr = 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 (isTokenStartRegExpStr.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..cc560768ac3ed 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 @@ -7,7 +7,7 @@ import { } from 'test/utils/pickers'; import { DateField } from '@mui/x-date-pickers/DateField'; -describeAdapters(' - Format', DateField, ({ adapter, renderWithProps }) => { +describeAdapters.only(' - Format', DateField, ({ adapter, renderWithProps }) => { it('should support escaped characters in start separator', () => { const { start: startChar, end: endChar } = adapter.escapedCharacters; @@ -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..32cf0a0653a41 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,31 @@ 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 isTokenStartRegExpStr = `^(${Object.keys(utils.formatTokenMap) + .sort((a, b) => b.length - a.length) // Sort to put longest word first + .join('|')})`; + + const getEscapedPartOfCurrentChar = (i: number) => + escapedParts.find((escapeIndex) => escapeIndex.start <= i && escapeIndex.end >= i); - let currentTokenValue = ''; + let i = 0; + while (i < expandedFormat.length) { + const escapedPartOfCurrentChar = getEscapedPartOfCurrentChar(i); - for (let i = 0; i < expandedFormat.length; i += 1) { - const escapedPartOfCurrentChar = escapedParts.find( - (escapeIndex) => escapeIndex.start <= i && escapeIndex.end >= i, + const isTokenStartRegExp = new RegExp( + isTokenStartRegExpStr, + 'g', // used to get access to lastIndex state ); - const char = expandedFormat[i]; const isEscapedChar = escapedPartOfCurrentChar != null; - const potentialToken = `${currentTokenValue}${expandedFormat.slice(i)}`; - const regExpMatch = isTokenStartRegExp.test(potentialToken); - - if (!isEscapedChar && char.match(/([A-Za-z]+)/) && regExpMatch) { - currentTokenValue = potentialToken.slice(0, isTokenStartRegExp.lastIndex); - i += isTokenStartRegExp.lastIndex - 1; + if (!isEscapedChar && isTokenStartRegExp.test(expandedFormat.slice(i))) { + const currentTokenValue = expandedFormat.slice(i, i + isTokenStartRegExp.lastIndex); + sections.push(createSection({ ...params, now, token: currentTokenValue, startSeparator })); + i += isTokenStartRegExp.lastIndex; } else { + const char = expandedFormat[i]; + // If we are on the opening or closing character of an escaped part of the format, // Then we ignore this character. const isEscapeBoundary = @@ -233,13 +234,6 @@ 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 { @@ -247,11 +241,9 @@ const buildSections = ( 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) { From 2d93032e36756c7e6d3c0a173093d097ee16566d Mon Sep 17 00:00:00 2001 From: delangle Date: Tue, 19 Mar 2024 10:46:31 +0100 Subject: [PATCH 2/4] Fix --- .../src/DateField/tests/format.DateField.test.tsx | 2 +- .../internals/hooks/useField/buildSectionsFromFormat.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) 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 cc560768ac3ed..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 @@ -7,7 +7,7 @@ import { } from 'test/utils/pickers'; import { DateField } from '@mui/x-date-pickers/DateField'; -describeAdapters.only(' - Format', DateField, ({ adapter, renderWithProps }) => { +describeAdapters(' - Format', DateField, ({ adapter, renderWithProps }) => { it('should support escaped characters in start separator', () => { const { start: startChar, end: endChar } = adapter.escapedCharacters; 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 32cf0a0653a41..cd96a5fbefd12 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts @@ -213,18 +213,23 @@ const buildSections = ( let i = 0; while (i < expandedFormat.length) { const escapedPartOfCurrentChar = getEscapedPartOfCurrentChar(i); + const isEscapedChar = escapedPartOfCurrentChar != null; const isTokenStartRegExp = new RegExp( isTokenStartRegExpStr, 'g', // used to get access to lastIndex state ); - const isEscapedChar = escapedPartOfCurrentChar != null; + // The remaining format starts with a token, + // We extract this token to create a new section. if (!isEscapedChar && isTokenStartRegExp.test(expandedFormat.slice(i))) { const currentTokenValue = expandedFormat.slice(i, i + isTokenStartRegExp.lastIndex); sections.push(createSection({ ...params, now, token: currentTokenValue, startSeparator })); i += isTokenStartRegExp.lastIndex; - } else { + } + // 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 we are on the opening or closing character of an escaped part of the format, From 95fd385e69e72b3314405fbfefa1f261824cc6db Mon Sep 17 00:00:00 2001 From: delangle Date: Tue, 19 Mar 2024 13:42:44 +0100 Subject: [PATCH 3/4] Work --- .../src/AdapterLuxon/AdapterLuxon.ts | 4 +- .../hooks/useField/buildSectionsFromFormat.ts | 37 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts index da7b2819add3a..066a0ab572b6e 100644 --- a/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts +++ b/packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts @@ -216,7 +216,7 @@ export class AdapterLuxon implements MuiPickersAdapter { // This RegExp tests if a string is only mad of supported tokens const validTokens = [...Object.keys(this.formatTokenMap), 'yyyyy']; - const isTokenStartRegExpStr = new RegExp(`(${validTokens.join('|')})*`); + 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; @@ -233,7 +233,7 @@ export class AdapterLuxon implements MuiPickersAdapter { return expandedToken.replace(catchWordsRegexp, (substring, g1, g2) => { const word = g1 || g2; // words are either in group 1 or group 2 - if (isTokenStartRegExpStr.test(word)) { + if (isWordComposedOfTokens.test(word)) { return substring; } return `'${substring}'`; 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 cd96a5fbefd12..4ad2ebd15b02b 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts @@ -203,9 +203,11 @@ const buildSections = ( let startSeparator: string = ''; // This RegExp tests if the beginning of a string corresponds to a supported token - const isTokenStartRegExpStr = `^(${Object.keys(utils.formatTokenMap) - .sort((a, b) => b.length - a.length) // Sort to put longest word first - .join('|')})`; + const validTokens = Object.keys(utils.formatTokenMap).sort((a, b) => b.length - a.length); // Sort to put longest word first + + const regExpFirstWordInFormat = /^([a-zA-Z]+)$/; + const regExpWordOnlyComposedOfTokens = new RegExp(`^(${validTokens.join('|')})*$`); + const regExpFirstTokenInWord = new RegExp(`^(${validTokens.join('|')})`); const getEscapedPartOfCurrentChar = (i: number) => escapedParts.find((escapeIndex) => escapeIndex.start <= i && escapeIndex.end >= i); @@ -214,18 +216,24 @@ const buildSections = ( while (i < expandedFormat.length) { const escapedPartOfCurrentChar = getEscapedPartOfCurrentChar(i); const isEscapedChar = escapedPartOfCurrentChar != null; + const firstWordInFormat = regExpFirstWordInFormat.exec(expandedFormat.slice(i))?.[1]; + + // The new 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 = ''; + } - const isTokenStartRegExp = new RegExp( - isTokenStartRegExpStr, - 'g', // used to get access to lastIndex state - ); - - // The remaining format starts with a token, - // We extract this token to create a new section. - if (!isEscapedChar && isTokenStartRegExp.test(expandedFormat.slice(i))) { - const currentTokenValue = expandedFormat.slice(i, i + isTokenStartRegExp.lastIndex); - sections.push(createSection({ ...params, now, token: currentTokenValue, startSeparator })); - i += isTokenStartRegExp.lastIndex; + 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. @@ -242,7 +250,6 @@ const buildSections = ( if (sections.length === 0) { startSeparator += char; } else { - startSeparator = ''; sections[sections.length - 1].endSeparator += char; } } From c9b77e1b9e72d75bdc75c1a30618643972007ffc Mon Sep 17 00:00:00 2001 From: delangle Date: Tue, 19 Mar 2024 13:58:18 +0100 Subject: [PATCH 4/4] Fix --- .../src/internals/hooks/useField/buildSectionsFromFormat.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4ad2ebd15b02b..63e05d31e7765 100644 --- a/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts +++ b/packages/x-date-pickers/src/internals/hooks/useField/buildSectionsFromFormat.ts @@ -205,7 +205,7 @@ const buildSections = ( // 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 - const regExpFirstWordInFormat = /^([a-zA-Z]+)$/; + const regExpFirstWordInFormat = /^([a-zA-Z]+)/; const regExpWordOnlyComposedOfTokens = new RegExp(`^(${validTokens.join('|')})*$`); const regExpFirstTokenInWord = new RegExp(`^(${validTokens.join('|')})`); @@ -218,7 +218,7 @@ const buildSections = ( const isEscapedChar = escapedPartOfCurrentChar != null; const firstWordInFormat = regExpFirstWordInFormat.exec(expandedFormat.slice(i))?.[1]; - // The new word in the format is only composed of tokens. + // The first word in the format is only composed of tokens. // We extract those tokens to create a new sections. if ( !isEscapedChar &&