diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 0f12e76ff9..292139fa58 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -94,9 +94,13 @@ export class Datetime implements ComponentInterface { private destroyCalendarIO?: () => void; private destroyKeyboardMO?: () => void; + private destroyOverlayListener?: () => void; private minParts?: any; private maxParts?: any; + private todayParts = parseDate(getToday()); + + private prevPresentation: string | null = null; /** * Duplicate reference to `activeParts` that does not trigger a re-render of the component. @@ -124,8 +128,6 @@ export class Datetime implements ComponentInterface { ampm: 'pm' } - private todayParts = parseDate(getToday()) - @Element() el!: HTMLIonDatetimeElement; @State() isPresented = false; @@ -483,8 +485,18 @@ export class Datetime implements ComponentInterface { this.confirm(); } + /** + * Stencil sometimes sets calendarBodyRef to null on rerender, even though + * the element is present. Query for it manually as a fallback. + * + * TODO(FW-901) Remove when issue is resolved: https://github.com/ionic-team/stencil/issues/3253 + */ + private getCalendarBodyEl = () => { + return this.calendarBodyRef || this.el.shadowRoot?.querySelector('.calendar-body'); + }; + private initializeKeyboardListeners = () => { - const { calendarBodyRef } = this; + const calendarBodyRef = this.getCalendarBodyEl(); if (!calendarBodyRef) { return; } const root = this.el!.shadowRoot!; @@ -530,7 +542,7 @@ export class Datetime implements ComponentInterface { * We must use keydown not keyup as we want * to prevent scrolling when using the arrow keys. */ - this.calendarBodyRef!.addEventListener('keydown', (ev: KeyboardEvent) => { + calendarBodyRef.addEventListener('keydown', (ev: KeyboardEvent) => { const activeElement = root.activeElement; if (!activeElement || !activeElement.classList.contains('calendar-day')) { return; } @@ -657,7 +669,7 @@ export class Datetime implements ComponentInterface { } private initializeCalendarIOListeners = () => { - const { calendarBodyRef } = this; + const calendarBodyRef = this.getCalendarBodyEl(); if (!calendarBodyRef) { return; } const mode = getIonMode(this); @@ -873,7 +885,7 @@ export class Datetime implements ComponentInterface { * listener. This is so that we can re-create the listeners * if the datetime has been hidden/presented by a modal or popover. */ - private destroyListeners = () => { + private destroyInteractionListeners = () => { const { destroyCalendarIO, destroyKeyboardMO } = this; if (destroyCalendarIO !== undefined) { @@ -885,6 +897,12 @@ export class Datetime implements ComponentInterface { } } + private initializeListeners() { + this.initializeCalendarIOListeners(); + this.initializeKeyboardListeners(); + this.initializeOverlayListener(); + } + componentDidLoad() { /** * If a scrollable element is hidden using `display: none`, @@ -898,9 +916,7 @@ export class Datetime implements ComponentInterface { const ev = entries[0]; if (!ev.isIntersecting) { return; } - this.initializeCalendarIOListeners(); - this.initializeKeyboardListeners(); - this.initializeOverlayListener(); + this.initializeListeners(); /** * TODO: Datetime needs a frame to ensure that it @@ -936,7 +952,7 @@ export class Datetime implements ComponentInterface { const ev = entries[0]; if (ev.isIntersecting) { return; } - this.destroyListeners(); + this.destroyInteractionListeners(); writeTask(() => { this.el.classList.remove('datetime-ready'); @@ -959,6 +975,29 @@ export class Datetime implements ComponentInterface { root.addEventListener('ionBlur', (ev: Event) => ev.stopPropagation()); } + /** + * When the presentation is changed, all calendar content is recreated, + * so we need to re-init behavior with the new elements. + */ + componentDidRender() { + const { presentation, prevPresentation } = this; + + if (prevPresentation === null) { + this.prevPresentation = presentation; + return; + } + + if (presentation === prevPresentation) { return; } + this.prevPresentation = presentation; + + this.destroyInteractionListeners(); + if (this.destroyOverlayListener !== undefined) { + this.destroyOverlayListener(); + } + + this.initializeListeners(); + } + /** * When doing subsequent presentations of an inline * overlay, the IO callback will fire again causing @@ -970,9 +1009,15 @@ export class Datetime implements ComponentInterface { const overlay = this.el.closest('ion-popover, ion-modal'); if (overlay === null) { return; } - overlay.addEventListener('willPresent', () => { + const overlayListener = () => { this.overlayIsPresenting = true; - }); + }; + + overlay.addEventListener('willPresent', overlayListener); + + this.destroyOverlayListener = () => { + overlay.removeEventListener('willPresent', overlayListener); + }; } private processValue = (value?: string | null) => { @@ -1034,7 +1079,7 @@ export class Datetime implements ComponentInterface { } private nextMonth = () => { - const { calendarBodyRef } = this; + const calendarBodyRef = this.getCalendarBodyEl(); if (!calendarBodyRef) { return; } const nextMonth = calendarBodyRef.querySelector('.calendar-month:last-of-type'); @@ -1050,7 +1095,7 @@ export class Datetime implements ComponentInterface { } private prevMonth = () => { - const { calendarBodyRef } = this; + const calendarBodyRef = this.getCalendarBodyEl(); if (!calendarBodyRef) { return; } const prevMonth = calendarBodyRef.querySelector('.calendar-month:first-of-type'); diff --git a/core/src/components/datetime/test/display/e2e.ts b/core/src/components/datetime/test/display/e2e.ts index e763b6b340..f6a587a4db 100644 --- a/core/src/components/datetime/test/display/e2e.ts +++ b/core/src/components/datetime/test/display/e2e.ts @@ -52,3 +52,41 @@ test('display', async () => { expect(screenshotCompare).toMatchScreenshot(); } }); + +test('month selection should work after changing presentation', async () => { + const page = await newE2EPage({ + url: '/src/components/datetime/test/display?ionic:_testing=true' + }); + const ionWorkingPartsDidChange = await page.spyOnEvent('ionWorkingPartsDidChange', 'document'); + let calendarMonthYear; + + await page.select('#presentation', 'date-time'); + await page.waitForChanges(); + + await page.select('#presentation', 'time-date'); + await page.waitForChanges(); + + const nextMonthButton = await page.find('ion-datetime >>> .calendar-next-prev ion-button + ion-button'); + await nextMonthButton.click(); + await page.waitForChanges(); + + await ionWorkingPartsDidChange.next(); + + calendarMonthYear = await page.find('ion-datetime >>> .calendar-month-year'); + + expect(calendarMonthYear.textContent).toContain('March 2022'); + + // ensure it still works if presentation is changed more than once + await page.select('#presentation', 'date-time'); + await page.waitForChanges(); + + const prevMonthButton = await page.find('ion-datetime >>> .calendar-next-prev ion-button:first-child'); + await prevMonthButton.click(); + await page.waitForChanges(); + + await ionWorkingPartsDidChange.next(); + + calendarMonthYear = await page.find('ion-datetime >>> .calendar-month-year'); + + expect(calendarMonthYear.textContent).toContain('February 2022'); +}); diff --git a/core/src/components/datetime/test/display/index.html b/core/src/components/datetime/test/display/index.html index a69f6a9136..b34d0d4d9b 100644 --- a/core/src/components/datetime/test/display/index.html +++ b/core/src/components/datetime/test/display/index.html @@ -1,52 +1,80 @@ -
- -