From 14f32f8feea7b3880367868ff0a2134b0c28cc07 Mon Sep 17 00:00:00 2001 From: Sean Perkins <13732623+sean-perkins@users.noreply.github.com> Date: Fri, 30 May 2025 14:11:26 -0400 Subject: [PATCH] fix(datetime): set working parts to last selected value (#29610) Issue number: resolves #29094 --------- ## What is the current behavior? When assigning multiple selected dates that span different months, the date time will not set the correct working parts and instead fallback to the default date: May 28, 2021. ## What is the new behavior? When opening the datetime with multiple dates selected, the calendar will animate to the last value in the array of selected dates. If the datetime is collapsed, body is not visible, is not a grid view or the user has made a selection of a new date, the calendar will not animate and instead will set the working parts to the current value selected (latest/last value). ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information Internally we discussed setting the month view to the first value in the array. Upon further investigation I determined this is not the expected behavior. When a user interacts with multiple date selection, the most recent selection (their active view) is the **last** value in the array. Animating or updating the working parts to the first value in the array would result in the calendar month jumping after every selection. Using the last index of the array results in no odd jumps in the experience. If a developer wishes to configure this behavior, they can change the order of the values in the value assigned to the datetime (to cause a specific month/date to be the initial view). Dev build: `8.5.8-dev.11748388365.11ad9dfe` --------- Co-authored-by: Maria Hutt --- core/src/components/datetime/datetime.tsx | 75 +++++++------------ .../datetime/test/multiple/datetime.e2e.ts | 49 +++++++++--- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index b63590c66b..ad9e9bc22e 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1263,21 +1263,20 @@ export class Datetime implements ComponentInterface { } /** - * If there are multiple values, pick an arbitrary one to clamp to. This way, - * if the values are across months, we always show at least one of them. Note - * that the values don't necessarily have to be in order. + * If there are multiple values, clamp to the last one. + * This is because the last value is the one that the user + * has most recently interacted with. */ - const singleValue = Array.isArray(valueToProcess) ? valueToProcess[0] : valueToProcess; + const singleValue = Array.isArray(valueToProcess) ? valueToProcess[valueToProcess.length - 1] : valueToProcess; const targetValue = clampDate(singleValue, minParts, maxParts); const { month, day, year, hour, minute } = targetValue; const ampm = parseAmPm(hour!); /** - * Since `activeParts` indicates a value that - * been explicitly selected either by the - * user or the app, only update `activeParts` - * if the `value` property is set. + * Since `activeParts` indicates a value that been explicitly selected + * either by the user or the app, only update `activeParts` if the + * `value` property is set. */ if (hasValue) { if (Array.isArray(valueToProcess)) { @@ -1301,53 +1300,29 @@ export class Datetime implements ComponentInterface { this.activeParts = []; } - /** - * Only animate if: - * 1. We're using grid style (wheel style pickers should just jump to new value) - * 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to) - * 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example) - * 4. The month/year picker is not open (since you wouldn't see the animation anyway) - */ const didChangeMonth = (month !== undefined && month !== workingParts.month) || (year !== undefined && year !== workingParts.year); const bodyIsVisible = el.classList.contains('datetime-ready'); const { isGridStyle, showMonthAndYear } = this; - let areAllSelectedDatesInSameMonth = true; - if (Array.isArray(valueToProcess)) { - const firstMonth = valueToProcess[0].month; - for (const date of valueToProcess) { - if (date.month !== firstMonth) { - areAllSelectedDatesInSameMonth = false; - break; - } - } - } - - /** - * If there is more than one date selected - * and the dates aren't all in the same month, - * then we should neither animate to the date - * nor update the working parts because we do - * not know which date the user wants to view. - */ - if (areAllSelectedDatesInSameMonth) { - if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) { - this.animateToDate(targetValue); - } else { - /** - * We only need to do this if we didn't just animate to a new month, - * since that calls prevMonth/nextMonth which calls setWorkingParts for us. - */ - this.setWorkingParts({ - month, - day, - year, - hour, - minute, - ampm, - }); - } + if (isGridStyle && didChangeMonth && bodyIsVisible && !showMonthAndYear) { + /** + * Only animate if: + * 1. We're using grid style (wheel style pickers should just jump to new value) + * 2. The month and/or year actually changed, and both are defined (otherwise there's nothing to animate to) + * 3. The calendar body is visible (prevents animation when in collapsed datetime-button, for example) + * 4. The month/year picker is not open (since you wouldn't see the animation anyway) + */ + this.animateToDate(targetValue); + } else { + this.setWorkingParts({ + month, + day, + year, + hour, + minute, + ampm, + }); } }; diff --git a/core/src/components/datetime/test/multiple/datetime.e2e.ts b/core/src/components/datetime/test/multiple/datetime.e2e.ts index 0e2c0efb38..55a386f6f2 100644 --- a/core/src/components/datetime/test/multiple/datetime.e2e.ts +++ b/core/src/components/datetime/test/multiple/datetime.e2e.ts @@ -174,18 +174,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(monthYear).toHaveText(/June 2022/); }); - test('should not scroll to new month when value is updated with dates in different months', async ({ page }) => { - const datetime = await datetimeFixture.goto(config, MULTIPLE_DATES); - await datetime.evaluate((el: HTMLIonDatetimeElement, dates: string[]) => { - el.value = dates; - }, MULTIPLE_DATES_SEPARATE_MONTHS); - - await page.waitForChanges(); - - const monthYear = datetime.locator('.calendar-month-year'); - await expect(monthYear).toHaveText(/June 2022/); - }); - test('with buttons, should only update value when confirm is called', async ({ page }) => { const datetime = await datetimeFixture.goto(config, SINGLE_DATE, { showDefaultButtons: true }); const june2Button = datetime.locator('[data-month="6"][data-day="2"]'); @@ -311,4 +299,41 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => { await expect(header).toHaveText('Mon, Oct 10'); }); }); + + test.describe('with selected days in different months', () => { + test(`set the active month view to the latest value's month`, async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/29094', + }); + + const datetime = await new DatetimeMultipleFixture(page).goto(config, MULTIPLE_DATES_SEPARATE_MONTHS); + const calendarMonthYear = datetime.locator('.calendar-month-year'); + + await expect(calendarMonthYear).toHaveText(/May 2022/); + }); + + test('does not change the active month view when selecting a day in a different month', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/29094', + }); + + const datetime = await new DatetimeMultipleFixture(page).goto(config, MULTIPLE_DATES_SEPARATE_MONTHS); + const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)'); + const calendarMonthYear = datetime.locator('.calendar-month-year'); + + await nextButton.click(); + + await expect(calendarMonthYear).toHaveText(/June 2022/); + + const june8Button = datetime.locator('[data-month="6"][data-day="8"]'); + + await june8Button.click(); + + await expect(calendarMonthYear).toHaveText(/June 2022/); + }); + }); });