mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 04:14:21 +08:00
fix(datetime): reinit behavior on presentation change (#24828)
Co-authored-by: Sean Perkins <sean@ionic.io>
This commit is contained in:
@ -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');
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -1,52 +1,80 @@
|
||||
<!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">
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
}
|
||||
ion-datetime {
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<label for="presentation">Presentation</label>
|
||||
<select id="presentation" onchange="changePresentation(event)">
|
||||
<option value="date-time" selected>date-time</option>
|
||||
<option value="time-date">time-date</option>
|
||||
<option value="date">date</option>
|
||||
<option value="time">time</option>
|
||||
</select>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Datetime - Standalone</title>
|
||||
<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>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
ion-datetime {
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<label for="presentation">Presentation</label>
|
||||
<select id="presentation" onchange="changePresentation(event)">
|
||||
<option value="date-time" selected>date-time</option>
|
||||
<option value="time-date">time-date</option>
|
||||
<option value="date">date</option>
|
||||
<option value="time">time</option>
|
||||
</select>
|
||||
|
||||
|
||||
<label for="size">Size</label>
|
||||
<select id="size" onchange="changeSize(event)">
|
||||
<option value="fixed" selected>fixed</option>
|
||||
<option value="cover">cover</option>
|
||||
</select>
|
||||
<label for="size">Size</label>
|
||||
<select id="size" onchange="changeSize(event)">
|
||||
<option value="fixed" selected>fixed</option>
|
||||
<option value="cover">cover</option>
|
||||
</select>
|
||||
|
||||
<br /><br />
|
||||
<br /><br />
|
||||
|
||||
<ion-datetime></ion-datetime>
|
||||
<ion-datetime value="2022-02-22"></ion-datetime>
|
||||
|
||||
<script>
|
||||
const datetime = document.querySelector('ion-datetime');
|
||||
<script>
|
||||
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>
|
||||
|
Reference in New Issue
Block a user