From f75977699dae5aeea3d97d4318377633e935afb9 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Fri, 15 Mar 2024 12:17:35 -0400 Subject: [PATCH] perf(datetime): calendar body shows immediately in modal on ios (#29163) Issue number: resolves #24542 --------- ## What is the current behavior? WebKit has a quirk where IntersectionObserver callbacks are delayed until after an accelerated animation finishes if the "root" specified in the config is the browser viewport (the default behavior if "root" is not specified) This means that when presenting a datetime in a modal on iOS the calendar body appears blank until the modal animation finishes. ## What is the new behavior? - We can work around this issue by observing an element inside of the datetime component and using the datetime component itself as the root. To do this, I added an `.intersection-tracker` element inside of datetime. This element has a dimension of 0x0 so it should not affect component layout or functionality. I opted to add this element instead of re-using an existing element because the existing elements are not guaranteed to always be in the DOM due to different datetime presentation styles. | `main` | branch | | - | - | | | | ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information Dev build: `7.8.1-dev.11710449785.14ebd5a0` --------- Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com> --- core/src/components/datetime/datetime.tsx | 25 +++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index f6a827d8e5..e385657d89 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -108,6 +108,7 @@ export class Datetime implements ComponentInterface { private calendarBodyRef?: HTMLElement; private monthYearToggleItemRef?: HTMLIonItemElement; private popoverRef?: HTMLIonPopoverElement; + private intersectionTrackerRef?: HTMLElement; private clearFocusVisible?: () => void; private parsedMinuteValues?: number[]; private parsedHourValues?: number[]; @@ -1079,6 +1080,8 @@ export class Datetime implements ComponentInterface { } componentDidLoad() { + const { el, intersectionTrackerRef } = this; + /** * If a scrollable element is hidden using `display: none`, * it will not have a scroll height meaning we cannot scroll elements @@ -1106,7 +1109,7 @@ export class Datetime implements ComponentInterface { this.el.classList.add('datetime-ready'); }); }; - const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01 }); + const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01, root: el }); /** * Use raf to avoid a race condition between the component loading and @@ -1114,7 +1117,7 @@ export class Datetime implements ComponentInterface { * could cause the datetime to start at a visibility of 0, erroneously * triggering the `hiddenIO` observer below. */ - raf(() => visibleIO?.observe(this.el)); + raf(() => visibleIO?.observe(intersectionTrackerRef!)); /** * We need to clean up listeners when the datetime is hidden @@ -1144,8 +1147,8 @@ export class Datetime implements ComponentInterface { this.el.classList.remove('datetime-ready'); }); }; - const hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0 }); - raf(() => hiddenIO?.observe(this.el)); + const hiddenIO = new IntersectionObserver(hiddenCallback, { threshold: 0, root: el }); + raf(() => hiddenIO?.observe(intersectionTrackerRef!)); /** * Datetime uses Ionic components that emit @@ -2613,6 +2616,20 @@ export class Datetime implements ComponentInterface { }), }} > + {/* + WebKit has a quirk where IntersectionObserver callbacks are delayed until after + an accelerated animation finishes if the "root" specified in the config is the + browser viewport (the default behavior if "root" is not specified). This means + that when presenting a datetime in a modal on iOS the calendar body appears + blank until the modal animation finishes. + + We can work around this by observing .intersection-tracker and using the host + (ion-datetime) as the "root". This allows the IO callback to fire the moment + the datetime is visible. The .intersection-tracker element should not have + dimensions or additional styles, and it should not be positioned absolutely + otherwise the IO callback may fire at unexpected times. + */} +
(this.intersectionTrackerRef = el)}>
{this.renderDatetime(mode)} );