Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fields] Support format without separator #12489

Merged
merged 4 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method was escaping tokens when they were not separated by any character.

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
Copy link
Member Author

@flaviendelangle flaviendelangle Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reworked this method because it was duplicating the 1st character of the 2nd token (once inside the token and once as a separator).

Since we catch the whole token in one passage in the loop, we can replace the for with a while and stop populating currentTokenValue incrementaly. This makes the whole behavior simplet IMHO.

Now we also support glued tokens (e.g: HHMM) which forces us to check if a word is composed only of tokens and if so to split them and create the sections.
The only thing we can't support is stuff like YYYYdeMM but I think it's a very very weird format 😬

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;
flaviendelangle marked this conversation as resolved.
Show resolved Hide resolved

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
Loading