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 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');
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user