Skip to content

Commit

Permalink
[fields] Support format without separator (mui#12489)
Browse files Browse the repository at this point in the history
  • Loading branch information
flaviendelangle authored and thomasmoon committed Sep 6, 2024
1 parent 1cefb59 commit dfa1160
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 35 deletions.
14 changes: 10 additions & 4 deletions packages/x-date-pickers/src/AdapterLuxon/AdapterLuxon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ export class AdapterLuxon implements MuiPickersAdapter<DateTime, string> {
// 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 (
Expand All @@ -225,12 +229,14 @@ export class AdapterLuxon implements MuiPickersAdapter<DateTime, string> {
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('')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ describeAdapters('<DateField /> - 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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,56 +202,60 @@ const buildSections = <TDate extends PickerValidDate>(
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 =
(isEscapedChar && escapedPartOfCurrentChar?.start === i) ||
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) {
Expand Down

0 comments on commit dfa1160

Please sign in to comment.