Skip to content

Commit

Permalink
perf(datetime): calendar body shows immediately in modal on ios (#29163)
Browse files Browse the repository at this point in the history
Issue number: resolves #24542

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

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?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- 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 |
| - | - |
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/e84d111d-b156-4f45-887a-d68a1097e5dd"></video>
| <video
src="https://github.com/ionic-team/ionic-framework/assets/2721089/3dccf1e5-cf79-46ab-b542-0537fd46fa76"></video>
|


## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `7.8.1-dev.11710449785.14ebd5a0`

---------

Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com>
  • Loading branch information
liamdebeasi and amandaejohnston committed Mar 15, 2024
1 parent fdfecd3 commit f759776
Showing 1 changed file with 21 additions and 4 deletions.
25 changes: 21 additions & 4 deletions core/src/components/datetime/datetime.tsx
Expand Up @@ -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[];
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1106,15 +1109,15 @@ 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
* its display animation starting (such as when shown in a modal). This
* 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/}
<div class="intersection-tracker" ref={(el) => (this.intersectionTrackerRef = el)}></div>
{this.renderDatetime(mode)}
</Host>
);
Expand Down

0 comments on commit f759776

Please sign in to comment.