Skip to content

Commit

Permalink
[fields] Do not clamp day of month (mui#9973)
Browse files Browse the repository at this point in the history
  • Loading branch information
flaviendelangle authored Aug 21, 2023
1 parent 019cc82 commit 2bd71a3
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ describe('<DateField /> - Editing', () => {
});
});

it("should set the day to the last day of today's month when no value is provided", () => {
it('should set the day to 31 when no value is provided', () => {
testFieldKeyPress({
format: adapter.formats.dayOfMonth,
key: 'ArrowDown',
expectedValue: '30',
expectedValue: '31',
});
});

Expand All @@ -68,6 +68,15 @@ describe('<DateField /> - Editing', () => {
});
});

it('should decrement the month and keep the day when the new month has fewer days', () => {
testFieldKeyPress({
format: adapter.formats.monthAndDate,
defaultValue: adapter.date(new Date(2022, 4, 31)),
key: 'ArrowDown',
expectedValue: 'April 31',
});
});

it('should go to the last day of the current month when a value in the first day of the month is provided', () => {
testFieldKeyPress({
format: adapter.formats.monthAndDate,
Expand Down Expand Up @@ -159,6 +168,15 @@ describe('<DateField /> - Editing', () => {
});
});

it('should increment the month and keep the day when the new month has fewer days', () => {
testFieldKeyPress({
format: adapter.formats.monthAndDate,
defaultValue: adapter.date(new Date(2022, 4, 31)),
key: 'ArrowUp',
expectedValue: 'June 31',
});
});

it('should go to the first day of the current month when a value in the last day of the month is provided', () => {
testFieldKeyPress({
format: adapter.formats.monthAndDate,
Expand Down Expand Up @@ -501,6 +519,21 @@ describe('<DateField /> - Editing', () => {
});
});

it('should allow to type the date 29th of February for leap years', () => {
testFieldChange({
format: adapter.formats.keyboardDate,
keyStrokes: [
{ value: '2/DD/YYYY', expected: '02/DD/YYYY' },
{ value: '02/2/YYYY', expected: '02/02/YYYY' },
{ value: '02/9/YYYY', expected: '02/29/YYYY' },
{ value: '02/29/1', expected: '02/29/0001' },
{ value: '02/29/9', expected: '02/29/0019' },
{ value: '02/29/8', expected: '02/29/0198' },
{ value: '02/29/8', expected: '02/29/1988' },
],
});
});

it('should not edit when props.readOnly = true and no value is provided', () => {
testFieldChange({
format: adapter.formats.year,
Expand Down Expand Up @@ -606,44 +639,6 @@ describe('<DateField /> - Editing', () => {
},
);

describeAdapters('Full editing scenarios', DateField, ({ adapter, renderWithProps }) => {
it('should move to the last day of the month when the current day exceeds it', () => {
const onChange = spy();

const { input, selectSection } = renderWithProps({ onChange });
selectSection('month');

fireEvent.change(input, { target: { value: '1/DD/YYYY' } }); // Press "1"
expectInputValue(input, '01/DD/YYYY');

fireEvent.change(input, { target: { value: '1/DD/YYYY' } }); // Press "1"
expectInputValue(input, '11/DD/YYYY');

fireEvent.change(input, { target: { value: '11/3/YYYY' } }); // Press "3"
expectInputValue(input, '11/03/YYYY');

fireEvent.change(input, { target: { value: '11/31/YYYY' } }); // Press "1"
expectInputValue(input, '11/31/YYYY');

// TODO: Fix this behavior on day.js (`clampDaySection` generates an invalid date for the start of the month).
if (adapter.lib === 'dayjs') {
return;
}

fireEvent.change(input, { target: { value: '11/31/2' } }); // Press "2"
expectInputValue(input, '11/30/0002'); // Has moved to the last day of the November

fireEvent.change(input, { target: { value: '11/30/0' } }); // Press "0"
expectInputValue(input, '11/30/0020');

fireEvent.change(input, { target: { value: '11/30/2' } }); // Press "2"
expectInputValue(input, '11/30/0202');

fireEvent.change(input, { target: { value: '11/30/2' } }); // Press "2"
expectInputValue(input, '11/30/2022');
});
});

describeAdapters('Pasting', DateField, ({ adapter, render, renderWithProps, clickOnInput }) => {
const firePasteEvent = (input: HTMLInputElement, pastedValue: string) => {
act(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -911,70 +911,6 @@ export const mergeDateIntoReferenceDate = <TDate>(

export const isAndroid = () => navigator.userAgent.toLowerCase().indexOf('android') > -1;

export const clampDaySectionIfPossible = <TDate, TSection extends FieldSection>(
utils: MuiPickersAdapter<TDate>,
timezone: PickersTimezone,
sections: TSection[],
sectionsValueBoundaries: FieldSectionsValueBoundaries<TDate>,
) => {
// We can only clamp the day value if:
// 1. if all the sections are filled (except the week day section which can be empty)
// 2. there is a day section
const canClamp =
sections.every((section) => section.type === 'weekDay' || section.value !== '') &&
sections.some((section) => section.type === 'day');

if (!canClamp) {
return null;
}

// We try to generate a valid date representing the start of the month of the invalid date typed by the user.
const sectionsForStartOfMonth = sections.map((section) => {
if (section.type !== 'day') {
return section;
}

const dayBoundaries = sectionsValueBoundaries.day({
currentDate: null,
format: section.format,
contentType: section.contentType,
});

return {
...section,
value: cleanDigitSectionValue(utils, timezone, dayBoundaries.minimum, dayBoundaries, section),
};
});

const startOfMonth = getDateFromDateSections(utils, sectionsForStartOfMonth);

// Even the start of the month is invalid, we probably have other invalid sections, the clamping failed.
if (startOfMonth == null || !utils.isValid(startOfMonth)) {
return null;
}

// The only invalid section was the day of the month, we replace its value with the maximum boundary for the correct month.
return sections.map((section) => {
if (section.type !== 'day') {
return section;
}

const dayBoundaries = sectionsValueBoundaries.day({
currentDate: startOfMonth,
format: section.format,
contentType: section.contentType,
});
if (Number(section.value) <= dayBoundaries.maximum) {
return section;
}

return {
...section,
value: dayBoundaries.maximum.toString(),
};
});
};

export const getSectionOrder = (
sections: FieldSectionWithoutPosition[],
isRTL: boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
import {
addPositionPropertiesToSections,
splitFormatIntoSections,
clampDaySectionIfPossible,
mergeDateIntoReferenceDate,
getSectionsBoundaries,
validateSections,
Expand Down Expand Up @@ -348,26 +347,7 @@ export const useFieldState = <
const activeDateManager = fieldValueManager.getActiveDateManager(utils, state, activeSection);
const newSections = setSectionValue(selectedSectionIndexes!.startIndex, newSectionValue);
const newActiveDateSections = activeDateManager.getSections(newSections);
let newActiveDate = getDateFromDateSections(utils, newActiveDateSections);
let shouldRegenSections = false;

/**
* If the date is invalid,
* Then we can try to clamp the day section to see if that produces a valid date.
* This can be useful if the month has fewer days than the day value currently provided.
*/
if (!utils.isValid(newActiveDate)) {
const clampedSections = clampDaySectionIfPossible(
utils,
timezone,
newActiveDateSections,
sectionsValueBoundaries,
);
if (clampedSections != null) {
shouldRegenSections = true;
newActiveDate = getDateFromDateSections(utils, clampedSections);
}
}
const newActiveDate = getDateFromDateSections(utils, newActiveDateSections);

let values: Pick<UseFieldState<TValue, TSection>, 'value' | 'referenceValue'>;
let shouldPublish: boolean;
Expand Down Expand Up @@ -396,25 +376,17 @@ export const useFieldState = <
(activeDateManager.date != null && !utils.isValid(activeDateManager.date));
}

/**
* If the value has been modified (to clamp the day).
* Then we need to re-generate the sections to make sure they also have this change.
*/
const sections = shouldRegenSections
? getSectionsFromValue(values.value, state.sections)
: newSections;

/**
* Publish or update the internal state with the new value and sections.
*/
if (shouldPublish) {
return publishValue({ ...values, sections });
return publishValue({ ...values, sections: newSections });
}

return setState((prevState) => ({
...prevState,
...values,
sections,
sections: newSections,
tempValueStrAndroid: null,
}));
};
Expand Down

0 comments on commit 2bd71a3

Please sign in to comment.