From bbcdfab729440ef76cfa15866f8997c968a19162 Mon Sep 17 00:00:00 2001 From: breken-ai <312387581+breken-ai@users.noreply.github.com> Date: Fri, 25 Sep 2026 19:41:10 -0700 Subject: [PATCH] fix(datetime): keep max when the day overflows validateParts clamped the date first and then fixed a day that does not exist in the month. clampDate returns the minParts or maxParts object itself, so the day fix was written into the bound. Selecting the 31st and then a shorter month on the wheel moved max to the last day of that month, and the returned date could be past max. Fix the day first, then clamp, and copy the clamped result. Resolves #29124 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../datetime/test/manipulation.spec.ts | 32 +++++++++++++++++++ .../components/datetime/utils/manipulation.ts | 16 ++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/core/src/components/datetime/test/manipulation.spec.ts b/core/src/components/datetime/test/manipulation.spec.ts index daebf7c33ea..d03cec325ba 100644 --- a/core/src/components/datetime/test/manipulation.spec.ts +++ b/core/src/components/datetime/test/manipulation.spec.ts @@ -558,6 +558,38 @@ describe('validateParts()', () => { }) ).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 }); }); + it('should not mutate the max parts when the day does not exist in the new month', () => { + // Selecting March 31 and then April on the month wheel produces April 31. + const maxParts = { month: 4, day: 20, year: 2024, hour: 23, minute: 59 }; + + expect(validateParts({ month: 4, day: 31, year: 2024, hour: 23, minute: 59 }, undefined, maxParts)).toEqual({ + month: 4, + day: 20, + year: 2024, + hour: 23, + minute: 59, + }); + expect(maxParts).toEqual({ month: 4, day: 20, year: 2024, hour: 23, minute: 59 }); + }); + it('should not mutate the min parts when the day does not exist in the new month', () => { + const minParts = { month: 3, day: 1, year: 2024, hour: 0, minute: 0 }; + + const result = validateParts({ month: 2, day: 31, year: 2024, hour: 0, minute: 0 }, minParts); + expect(result).toEqual({ month: 3, day: 1, year: 2024, hour: 0, minute: 0 }); + expect(minParts).toEqual({ month: 3, day: 1, year: 2024, hour: 0, minute: 0 }); + }); + it('should move the time back in bounds when the fixed day lands on the max day', () => { + // February 31 becomes February 29, which is the max day, so the max time applies. + expect( + validateParts({ month: 2, day: 31, year: 2024, hour: 23, minute: 0 }, undefined, { + month: 2, + day: 29, + year: 2024, + hour: 20, + minute: 0, + }) + ).toEqual({ month: 2, day: 29, year: 2024, hour: 20, minute: 0 }); + }); }); describe('getClosestValidDate()', () => { diff --git a/core/src/components/datetime/utils/manipulation.ts b/core/src/components/datetime/utils/manipulation.ts index 152f2f62fec..6c954efd251 100644 --- a/core/src/components/datetime/utils/manipulation.ts +++ b/core/src/components/datetime/utils/manipulation.ts @@ -342,7 +342,7 @@ export const validateParts = ( maxParts?: DatetimeParts ): DatetimeParts => { const { month, day, year } = parts; - const partsCopy = clampDate({ ...parts }, minParts, maxParts); + const dayInMonth = { ...parts }; const numDays = getNumDaysInMonth(month, year); @@ -351,11 +351,21 @@ export const validateParts = ( * is greater than the day we want * to set, update the DatetimeParts * day field to be the max days. + * This is done before clamping so that + * the fixed day is also checked against + * the min/max bounds. */ if (day !== null && numDays < day) { - partsCopy.day = numDays; + dayInMonth.day = numDays; } + /** + * clampDate can return the minParts or maxParts + * object itself, so copy the result to avoid + * changing the bounds when updating the time below. + */ + const partsCopy = { ...clampDate(dayInMonth, minParts, maxParts) }; + /** * If value is same day as min day, * make sure the time value is in bounds. @@ -391,7 +401,7 @@ export const validateParts = ( * If value is same day as max day, * make sure the time value is in bounds. */ - if (maxParts !== undefined && isSameDay(parts, maxParts)) { + if (maxParts !== undefined && isSameDay(partsCopy, maxParts)) { /** * If the hour is out of bounds, * update both the hour and minute.