From 11f44e94f4abe81892f33a057055e5f9b5092528 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 23 Sep 2022 08:31:39 -0500 Subject: [PATCH] fix(datetime): switching month and year accounts for day (#25996) resolves #25585 --- core/src/components/datetime/datetime.tsx | 22 +++++++-- .../components/datetime/test/datetime.e2e.ts | 48 +++++++++++++++++++ .../components/datetime/utils/manipulation.ts | 24 ++++++++++ 3 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 core/src/components/datetime/test/datetime.e2e.ts diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 0963d867df..2ec37b36d9 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -45,6 +45,7 @@ import { getPreviousWeek, getPreviousYear, getStartOfWeek, + validateParts, } from './utils/manipulation'; import { clampDate, @@ -583,6 +584,19 @@ export class Datetime implements ComponentInterface { private setActiveParts = (parts: DatetimeParts, removeDate = false) => { const { multiple, activePartsClone, highlightActiveParts } = this; + /** + * When setting the active parts, it is possible + * to set invalid data. For example, + * when updating January 31 to February, + * February 31 does not exist. As a result + * we need to validate the active parts and + * ensure that we are only setting valid dates. + * Additionally, we need to update the working parts + * too in the event that the validated parts are different. + */ + const validatedParts = validateParts(parts); + this.setWorkingParts(validatedParts); + if (multiple) { /** * We read from activePartsClone here because valueChanged() only updates that, @@ -595,20 +609,20 @@ export class Datetime implements ComponentInterface { */ const activePartsArray = Array.isArray(activePartsClone) ? activePartsClone : [activePartsClone]; if (removeDate) { - this.activeParts = activePartsArray.filter((p) => !isSameDay(p, parts)); + this.activeParts = activePartsArray.filter((p) => !isSameDay(p, validatedParts)); } else if (highlightActiveParts) { - this.activeParts = [...activePartsArray, parts]; + this.activeParts = [...activePartsArray, validatedParts]; } else { /** * If highlightActiveParts is false, that means we just have a * default value of today in activeParts; we need to replace that * rather than adding to it since it's just a placeholder. */ - this.activeParts = [parts]; + this.activeParts = [validatedParts]; } } else { this.activeParts = { - ...parts, + ...validatedParts, }; } diff --git a/core/src/components/datetime/test/datetime.e2e.ts b/core/src/components/datetime/test/datetime.e2e.ts new file mode 100644 index 0000000000..e212b5d07d --- /dev/null +++ b/core/src/components/datetime/test/datetime.e2e.ts @@ -0,0 +1,48 @@ +import { expect } from '@playwright/test'; +import { test } from '@utils/test/playwright'; + +test.describe('datetime: switching months with different number of days', () => { + test.beforeEach(async ({ page, skip }) => { + skip.rtl(); + skip.mode('ios'); + + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + }); + + test('should switch the calendar header when moving to a month with a different number of days', async ({ page }) => { + const monthYearToggle = page.locator('ion-datetime .calendar-month-year'); + const monthColumnItems = page.locator('ion-datetime .month-column .picker-item:not(.picker-item-empty)'); + + await expect(monthYearToggle).toContainText('January 2022'); + + await monthYearToggle.click(); + await page.waitForChanges(); + + // February + await monthColumnItems.nth(1).click(); + await page.waitForChanges(); + + await expect(monthYearToggle).toContainText('February 2022'); + }); + + test('should adjust the selected day when moving to a month with a different number of days', async ({ page }) => { + const monthYearToggle = page.locator('ion-datetime .calendar-month-year'); + const monthColumnItems = page.locator('ion-datetime .month-column .picker-item:not(.picker-item-empty)'); + const datetime = page.locator('ion-datetime'); + const ionChange = await page.spyOnEvent('ionChange'); + + await monthYearToggle.click(); + await page.waitForChanges(); + + // February + await monthColumnItems.nth(1).click(); + + await ionChange.next(); + await expect(ionChange).toHaveReceivedEventTimes(1); + await expect(datetime).toHaveJSProperty('value', '2022-02-28'); + }); +}); diff --git a/core/src/components/datetime/utils/manipulation.ts b/core/src/components/datetime/utils/manipulation.ts index 50d019ac16..98f25750af 100644 --- a/core/src/components/datetime/utils/manipulation.ts +++ b/core/src/components/datetime/utils/manipulation.ts @@ -339,3 +339,27 @@ export const calculateHourFromAMPM = (currentParts: DatetimeParts, newAMPM: 'am' return newHour; }; + +/** + * Updates parts to ensure that month and day + * values are valid. For days that do not exist, + * the closest valid day is used. + */ +export const validateParts = (parts: DatetimeParts): DatetimeParts => { + const { month, day, year } = parts; + const partsCopy = { ...parts }; + + const numDays = getNumDaysInMonth(month, year); + + /** + * If the max number of days + * is greater than the day we want + * to set, update the DatetimeParts + * day field to be the max days. + */ + if (day !== null && numDays < day) { + partsCopy.day = numDays; + } + + return partsCopy; +};