fix(datetime): scroll position no longer gets reset when using datetime in overlay (#23543)

This commit is contained in:
Liam DeBeasi
2021-07-01 09:22:59 -04:00
committed by GitHub
parent bdb95b7b6d
commit b735b587cd
2 changed files with 149 additions and 10 deletions

View File

@ -87,6 +87,11 @@ export class Datetime implements ComponentInterface {
private parsedYearValues?: number[];
private parsedDayValues?: number[];
private destroyCalendarIO?: () => void;
private destroyKeyboardMO?: () => void;
private destroyTimeScroll?: () => void;
private destroyMonthAndYearScroll?: () => void;
private minParts?: any;
private maxParts?: any;
@ -441,6 +446,10 @@ export class Datetime implements ComponentInterface {
const mo = new MutationObserver(checkCalendarBodyFocus);
mo.observe(calendarBodyRef, { attributeFilter: ['class'], attributeOldValue: true });
this.destroyKeyboardMO = () => {
mo?.disconnect();
}
/**
* We must use keydown not keyup as we want
* to prevent scrolling when using the arrow keys.
@ -729,6 +738,11 @@ export class Datetime implements ComponentInterface {
root: calendarBodyRef
});
startIO.observe(startMonth);
this.destroyCalendarIO = () => {
endIO?.disconnect();
startIO?.disconnect();
}
});
}
@ -743,6 +757,31 @@ export class Datetime implements ComponentInterface {
}
}
/**
* Clean up all listeners except for the overlay
* 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 = () => {
const { destroyCalendarIO, destroyKeyboardMO, destroyTimeScroll, destroyMonthAndYearScroll } = this;
if (destroyCalendarIO !== undefined) {
destroyCalendarIO();
}
if (destroyKeyboardMO !== undefined) {
destroyKeyboardMO();
}
if (destroyTimeScroll !== undefined) {
destroyTimeScroll();
}
if (destroyMonthAndYearScroll !== undefined) {
destroyMonthAndYearScroll();
}
}
componentDidLoad() {
const mode = getIonMode(this);
@ -758,11 +797,6 @@ export class Datetime implements ComponentInterface {
const ev = entries[0];
if (!ev.isIntersecting) { return; }
/**
* This needs to run at most once for initial setup.
*/
visibleIO!.disconnect()
this.initializeCalendarIOListeners();
this.initializeKeyboardListeners();
this.initializeTimeScrollListener();
@ -786,6 +820,27 @@ export class Datetime implements ComponentInterface {
}
visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01 });
visibleIO.observe(this.el);
/**
* We need to clean up listeners when the datetime is hidden
* in a popover/modal so that we can properly scroll containers
* back into view if they are re-presented. When the datetime is hidden
* the scroll areas have scroll widths/heights of 0px, so any snapping
* we did originally has been lost.
*/
let hiddenIO: IntersectionObserver | undefined;
const hiddenCallback = (entries: IntersectionObserverEntry[]) => {
const ev = entries[0];
if (ev.isIntersecting) { return; }
this.destroyListeners();
writeTask(() => {
this.el.classList.remove('datetime-ready');
});
}
hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0 });
hiddenIO.observe(this.el);
}
/**
@ -897,8 +952,15 @@ export class Datetime implements ComponentInterface {
* does not fire when we do our initial scrollIntoView above.
*/
raf(() => {
monthRef.addEventListener('scroll', () => scrollCallback('month'));
yearRef.addEventListener('scroll', () => scrollCallback('year'));
const monthScroll = () => scrollCallback('month');
const yearScroll = () => scrollCallback('year');
monthRef.addEventListener('scroll', monthScroll);
yearRef.addEventListener('scroll', yearScroll);
this.destroyMonthAndYearScroll = () => {
monthRef.removeEventListener('scroll', monthScroll);
yearRef.removeEventListener('scroll', yearScroll);
}
});
}
@ -979,8 +1041,16 @@ export class Datetime implements ComponentInterface {
* does not fire when we do our initial scrollIntoView above.
*/
raf(() => {
timeHourRef.addEventListener('scroll', () => scrollCallback('hour'));
timeMinuteRef.addEventListener('scroll', () => scrollCallback('minute'));
const hourScroll = () => scrollCallback('hour');
const minuteScroll = () => scrollCallback('minute');
timeHourRef.addEventListener('scroll', hourScroll);
timeMinuteRef.addEventListener('scroll', minuteScroll);
this.destroyTimeScroll = () => {
timeHourRef.removeEventListener('scroll', hourScroll);
timeMinuteRef.removeEventListener('scroll', minuteScroll);
}
});
});
}
@ -1076,7 +1146,7 @@ export class Datetime implements ComponentInterface {
<slot name="buttons">
<ion-buttons>
<ion-button color={this.color} onClick={() => this.cancel(true)}>{this.cancelText}</ion-button>
<ion-button color={this.color} onClick={() => this.confirm()}>{this.doneText}</ion-button>
<ion-button color={this.color} onClick={() => this.confirm(true)}>{this.doneText}</ion-button>
</ion-buttons>
</slot>
</div>

View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - Cover</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>
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(250px, 1fr));
grid-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
ion-modal {
--width: 350px;
}
.placeholder {
color: #777;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Cover</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item button="true" id="datetime-trigger">
<ion-label>Birthday</ion-label>
<ion-text class="placeholder">Select a date</ion-text>
</ion-item>
<ion-modal trigger="datetime-trigger">
<ion-datetime
show-default-title="true"
show-default-buttons="true"
></ion-datetime>
</ion-modal>
</ion-content>
<script>
const datetime = document.querySelector('ion-datetime');
const text = document.querySelector('ion-text');
datetime.addEventListener('ionChange', (ev) => {
const date = new Date(ev.detail.value);
text.innerText = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format(date);
text.classList.remove('placeholder');
});
</script>
</ion-app>
</body>
</html>