Skip to content

fix(datetime): prevent hidden-state observer from tearing down ready class on initial entry - #31108

Merged
ShaneK merged 1 commit into
mainfrom
FW-7284
May 1, 2026
Merged

ShaneK merged 1 commit into
mainfrom
FW-7284

Conversation

@ShaneK

@ShaneK ShaneK commented Apr 30, 2026

Copy link
Copy Markdown
Member

Issue number: internal


What is the current behavior?

ion-datetime runs two IntersectionObservers: one to detect when the host becomes visible (which adds datetime-ready) and one to detect when it becomes hidden (which removes the class and tears down listeners). When the host mounts offscreen, both observers receive an initial "not intersecting" entry on observe(). The hidden-state observer treats that initial entry as a real visible-to-hidden transition, queues a writeTask to remove datetime-ready, and races the layout-based fallback (ensureReadyIfVisible) that adds the class after 100ms. On WebKit the remove wins often enough that the e2e test for the fallback (added in #30793 to fix #30706) had to be skipped on Mobile Safari. Anything in production that adds datetime-ready outside of a real isIntersecting: true event is exposed to the same race.

What is the new behavior?

A hasBeenIntersecting flag is set true only when visibleCallback observes isIntersecting: true. The hidden-state observer's teardown is gated on this flag, so the synthetic initial "not intersecting" entry is ignored. The flag is reset when the host actually transitions to hidden and on disconnectedCallback. The previously duplicated init-listeners + ready-class block is consolidated into a single markReady helper. The WebKit skip on the IO-fallback e2e test has been removed.

Does this introduce a breaking change?

  • Yes
  • No

Other information

The asymmetry where ensureReadyIfVisible (the layout fallback) deliberately does NOT set hasBeenIntersecting is load-bearing: the flag must reflect a real observer signal, not a fallback-driven write, otherwise the bug returns. This is called out at the guard site so future cleanups don't undo it.

This test was most likely to fail in docker testing Linux Webkit with --repeat-each=20 because it was pretty flaky. I was able to force it to fail under these conditions and, after fixing it, it no longer failed.

Relevant Preview Link:

@ShaneK
ShaneK requested a review from a team as a code owner April 30, 2026 19:51
@ShaneK
ShaneK requested a review from thetaPC April 30, 2026 19:51
@vercel

vercel Bot commented Apr 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ionic-framework Ready Ready Preview, Comment Apr 30, 2026 7:52pm

Request Review

@github-actions github-actions Bot added the package: core @ionic/core package label Apr 30, 2026

@thetaPC thetaPC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@ShaneK
ShaneK added this pull request to the merge queue May 1, 2026
Merged via the queue into main with commit 30b479a May 1, 2026
52 checks passed
@ShaneK
ShaneK deleted the FW-7284 branch May 1, 2026 16:38
pull Bot pushed a commit to LoadsAForks/ionic-framework that referenced this pull request Aug 7, 2026
…-team#31335)

Issue number: resolves ionic-team#31155, resolves ionic-team#31143

---------

## What is the current behavior?

Currently, an `ion-datetime` inside a modal or popover shows the wrong
month once the overlay is reopened. The selected day isn't visible, the
previous month button does nothing, and picking a day from the grid
lands on an unrelated date.

Overlays move their host element into `ion-app` when presenting and back
to its original position when dismissing, which disconnects and
reconnects the datetime. `disconnectedCallback` reset
`hasBeenIntersecting` during that move, so by the time the hidden-state
`IntersectionObserver` entry arrived, `hiddenCallback` mistook the
dismissal for the synthetic initial entry and returned early. That left
`datetime-ready` on the host, so on the next present `markReady` saw the
class and returned without re-centering the calendar on the working
month, and the browser had already reset `scrollLeft` to 0 while the
overlay was hidden. `scrollLeft: 0` renders the previous month's grid
while the header still names the working month, which is what produces
all three symptoms.

## What is the new behavior?

With this change, `disconnectedCallback` no longer resets
`hasBeenIntersecting`. That flag tracks the observers, and the observers
are only created in `componentDidLoad` and never re-created on
reconnect, so a DOM move has no business clearing it. `hiddenCallback`
now sees the real hidden transition on dismiss and tears down as it did
before ionic-team#31108, which lets `markReady` run again on the next present and
re-center the calendar.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

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

This regression was introduced in ionic-team#31108. That PR needed the flag to
make `hiddenCallback` ignore the synthetic initial entry, but the
`disconnectedCallback` reset it also added had no job and broke the
overlay case. Before ionic-team#31108, `hiddenCallback` had no guard at all and
always removed `datetime-ready` on dismiss, so this restores the
behavior that shipped for all of v8..

- [Relevant test screen -
iOS](https://ionic-framework-git-fix-31155-ionic1.vercel.app/src/components/datetime-button/test/overlays?ionic:mode=ios)
- [Relevant test screen -
MD](https://ionic-framework-git-fix-31155-ionic1.vercel.app/src/components/datetime-button/test/overlays?ionic:mode=md)

To reproduce: open the "Modal - Default" picker, dismiss it, then open
it again. On `main` the grid shows February while the header reads March
2022.

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
pull Bot pushed a commit to LoadsAForks/ionic-framework that referenced this pull request Sep 21, 2026
…nic-team#31460)

Issue number: resolves ionic-team#30933

---------

## What is the current behavior?

Currently, an `ion-datetime` in a modal or popover sometimes opens with
a blank calendar on iOS 26. The host is missing `datetime-ready`, so
`.calendar-body` stays at `opacity: 0`. It doesn't happen every time,
and when it does the calendar stays blank until the overlay is reopened.

The `ion-datetime` component runs two IntersectionObservers on the same
root and target, one that adds `datetime-ready` and one that removes it.
WebKit reports an element that is still on screen as not intersecting,
and it doesn't deliver that entry to every observer, so the removing
observer tears the ready state down and the adding one never hears the
recovery.

## What is the new behavior?

The hidden-state observer now checks the host before tearing anything
down. An overlay hides its contents with `display: none`, which leaves
the host without a layout box, so a host that still has one is on screen
and the entry is wrong. The `hasBeenIntersecting` flag added in ionic-team#31108
is gone, because the same check covers the synthetic "not intersecting"
entry that `observe()` fires when the host mounts offscreen. Both
callbacks also read the last entry instead of the first, since a batched
callback's first entry can be stale.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

## Other information

I reproduced this on an iOS 26.2 simulator, opening and dismissing the
modal in a loop and checking `datetime-ready` after each open. Four
extra observers on the same root and target recorded 54, 52, 54, 52
events over 25 cycles, which is where the per-observer delivery claim
comes from.

Same machine and everything, 150 cycles each:

| build | blank calendar |
| --- | --- |
| main | 7 (4.7%) |
| this branch | 0 |

The e2e test stubs `IntersectionObserver` to report hidden while the
datetime is on screen, since the WebKit misbehavior can't be forced on
demand. It fails on all three browsers without the fix. The open/close
cycle test guards against regressions but won't reproduce the glitch on
its own.

[Relevant test screen -
iOS](https://ionic-framework-git-fix-30933-ionic1.vercel.app/src/components/datetime-button/test/overlays?ionic:mode=ios)

This branch was successfully deployed

1 active deployment
Preview 6dfda17a Deployed Apr 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: core @ionic/core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: IonDateTime sometimes show invisible calendar

2 participants