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 destroyCalendarIO?: () => void;
private destroyKeyboardMO?: () => void; private destroyKeyboardMO?: () => void;
private destroyOverlayListener?: () => void;
private minParts?: any; private minParts?: any;
private maxParts?: 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. * 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' ampm: 'pm'
} }
private todayParts = parseDate(getToday())
@Element() el!: HTMLIonDatetimeElement; @Element() el!: HTMLIonDatetimeElement;
@State() isPresented = false; @State() isPresented = false;
@ -483,8 +485,18 @@ export class Datetime implements ComponentInterface {
this.confirm(); 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 = () => { private initializeKeyboardListeners = () => {
const { calendarBodyRef } = this; const calendarBodyRef = this.getCalendarBodyEl();
if (!calendarBodyRef) { return; } if (!calendarBodyRef) { return; }
const root = this.el!.shadowRoot!; const root = this.el!.shadowRoot!;
@ -530,7 +542,7 @@ export class Datetime implements ComponentInterface {
* We must use keydown not keyup as we want * We must use keydown not keyup as we want
* to prevent scrolling when using the arrow keys. * to prevent scrolling when using the arrow keys.
*/ */
this.calendarBodyRef!.addEventListener('keydown', (ev: KeyboardEvent) => { calendarBodyRef.addEventListener('keydown', (ev: KeyboardEvent) => {
const activeElement = root.activeElement; const activeElement = root.activeElement;
if (!activeElement || !activeElement.classList.contains('calendar-day')) { return; } if (!activeElement || !activeElement.classList.contains('calendar-day')) { return; }
@ -657,7 +669,7 @@ export class Datetime implements ComponentInterface {
} }
private initializeCalendarIOListeners = () => { private initializeCalendarIOListeners = () => {
const { calendarBodyRef } = this; const calendarBodyRef = this.getCalendarBodyEl();
if (!calendarBodyRef) { return; } if (!calendarBodyRef) { return; }
const mode = getIonMode(this); const mode = getIonMode(this);
@ -873,7 +885,7 @@ export class Datetime implements ComponentInterface {
* listener. This is so that we can re-create the listeners * listener. This is so that we can re-create the listeners
* if the datetime has been hidden/presented by a modal or popover. * if the datetime has been hidden/presented by a modal or popover.
*/ */
private destroyListeners = () => { private destroyInteractionListeners = () => {
const { destroyCalendarIO, destroyKeyboardMO } = this; const { destroyCalendarIO, destroyKeyboardMO } = this;
if (destroyCalendarIO !== undefined) { if (destroyCalendarIO !== undefined) {
@ -885,6 +897,12 @@ export class Datetime implements ComponentInterface {
} }
} }
private initializeListeners() {
this.initializeCalendarIOListeners();
this.initializeKeyboardListeners();
this.initializeOverlayListener();
}
componentDidLoad() { componentDidLoad() {
/** /**
* If a scrollable element is hidden using `display: none`, * If a scrollable element is hidden using `display: none`,
@ -898,9 +916,7 @@ export class Datetime implements ComponentInterface {
const ev = entries[0]; const ev = entries[0];
if (!ev.isIntersecting) { return; } if (!ev.isIntersecting) { return; }
this.initializeCalendarIOListeners(); this.initializeListeners();
this.initializeKeyboardListeners();
this.initializeOverlayListener();
/** /**
* TODO: Datetime needs a frame to ensure that it * TODO: Datetime needs a frame to ensure that it
@ -936,7 +952,7 @@ export class Datetime implements ComponentInterface {
const ev = entries[0]; const ev = entries[0];
if (ev.isIntersecting) { return; } if (ev.isIntersecting) { return; }
this.destroyListeners(); this.destroyInteractionListeners();
writeTask(() => { writeTask(() => {
this.el.classList.remove('datetime-ready'); this.el.classList.remove('datetime-ready');
@ -959,6 +975,29 @@ export class Datetime implements ComponentInterface {
root.addEventListener('ionBlur', (ev: Event) => ev.stopPropagation()); 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 * When doing subsequent presentations of an inline
* overlay, the IO callback will fire again causing * 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'); const overlay = this.el.closest('ion-popover, ion-modal');
if (overlay === null) { return; } if (overlay === null) { return; }
overlay.addEventListener('willPresent', () => { const overlayListener = () => {
this.overlayIsPresenting = true; this.overlayIsPresenting = true;
}); };
overlay.addEventListener('willPresent', overlayListener);
this.destroyOverlayListener = () => {
overlay.removeEventListener('willPresent', overlayListener);
};
} }
private processValue = (value?: string | null) => { private processValue = (value?: string | null) => {
@ -1034,7 +1079,7 @@ export class Datetime implements ComponentInterface {
} }
private nextMonth = () => { private nextMonth = () => {
const { calendarBodyRef } = this; const calendarBodyRef = this.getCalendarBodyEl();
if (!calendarBodyRef) { return; } if (!calendarBodyRef) { return; }
const nextMonth = calendarBodyRef.querySelector('.calendar-month:last-of-type'); const nextMonth = calendarBodyRef.querySelector('.calendar-month:last-of-type');
@ -1050,7 +1095,7 @@ export class Datetime implements ComponentInterface {
} }
private prevMonth = () => { private prevMonth = () => {
const { calendarBodyRef } = this; const calendarBodyRef = this.getCalendarBodyEl();
if (!calendarBodyRef) { return; } if (!calendarBodyRef) { return; }
const prevMonth = calendarBodyRef.querySelector('.calendar-month:first-of-type'); const prevMonth = calendarBodyRef.querySelector('.calendar-month:first-of-type');

View File

@ -52,3 +52,41 @@ test('display', async () => {
expect(screenshotCompare).toMatchScreenshot(); 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,52 +1,80 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" dir="ltr"> <html lang="en" dir="ltr">
<head>
<meta charset="UTF-8"> <head>
<title>Datetime - Standalone</title> <meta charset="UTF-8">
<meta name="viewport" <title>Datetime - Standalone</title>
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="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet"> <link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script> <script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script> <script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style> <style>
body { body {
padding: 20px; padding: 20px;
} }
ion-datetime {
border: 1px solid black; ion-datetime {
} border: 1px solid black;
</style> }
</head> </style>
<body> </head>
<label for="presentation">Presentation</label>
<select id="presentation" onchange="changePresentation(event)"> <body>
<option value="date-time" selected>date-time</option> <label for="presentation">Presentation</label>
<option value="time-date">time-date</option> <select id="presentation" onchange="changePresentation(event)">
<option value="date">date</option> <option value="date-time" selected>date-time</option>
<option value="time">time</option> <option value="time-date">time-date</option>
</select> <option value="date">date</option>
<option value="time">time</option>
</select>
<label for="size">Size</label> <label for="size">Size</label>
<select id="size" onchange="changeSize(event)"> <select id="size" onchange="changeSize(event)">
<option value="fixed" selected>fixed</option> <option value="fixed" selected>fixed</option>
<option value="cover">cover</option> <option value="cover">cover</option>
</select> </select>
<br /><br /> <br /><br />
<ion-datetime></ion-datetime> <ion-datetime value="2022-02-22"></ion-datetime>
<script> <script>
const datetime = document.querySelector('ion-datetime'); const datetime = document.querySelector('ion-datetime');
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>
const changePresentation = (ev) => {
datetime.presentation = ev.target.value;
}
const changeSize = (ev) => {
datetime.size = ev.target.value;
}
</script>
</body>
</html> </html>