fix(datetime): reinit behavior on presentation change (#24828)

Co-authored-by: Sean Perkins <sean@ionic.io>
This commit is contained in:
Amanda Smith
2022-03-11 15:45:46 -06:00
committed by GitHub
parent 981eeba0e1
commit d46e1e8506
3 changed files with 168 additions and 57 deletions

View File

@ -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');

View File

@ -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');
});

View File

@ -1,10 +1,10 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - Standalone</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
@ -13,11 +13,13 @@
body {
padding: 20px;
}
ion-datetime {
border: 1px solid black;
}
</style>
</head>
<body>
<label for="presentation">Presentation</label>
<select id="presentation" onchange="changePresentation(event)">
@ -36,17 +38,43 @@
<br /><br />
<ion-datetime></ion-datetime>
<ion-datetime value="2022-02-22"></ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
const changePresentation = (ev) => {
datetime.presentation = ev.target.value;
const mutationObserver = new MutationObserver(() => {
document.dispatchEvent(new CustomEvent('ionWorkingPartsDidChange'));
});
const initCalendarMonthChangeObserver = async () => {
if (!datetime.componentOnReady) return;
await datetime.componentOnReady();
// We have to requestAnimationFrame to allow the datetime to render completely.
requestAnimationFrame(() => {
const calendarBody = datetime.shadowRoot.querySelector('.calendar-body');
if (calendarBody) {
mutationObserver.observe(calendarBody, {
childList: true,
subtree: true
});
}
});
}
const changePresentation = (ev) => {
mutationObserver.disconnect();
datetime.presentation = ev.target.value;
initCalendarMonthChangeObserver();
};
const changeSize = (ev) => {
datetime.size = ev.target.value;
}
};
initCalendarMonthChangeObserver();
</script>
</body>
</html>