Skip to content

bug: ion-datetime datetime-ready gets stuck after the element is relocated in the DOM, so it never re-initializes on re-show #31281

Description

@nf-matt

Prerequisites

Ionic Framework Version

v8.x

Current Behavior

If a single ion-datetime element is reused across presentations by moving it between DOM parents (i.e. it gets disconnectedCallbackconnectedCallback between shows), the calendar/wheel stops repainting its selection on the second and every subsequent show. activeParts is correct and reset(startDate) is being called with a valid date, but the previously-selected day is not rendered as selected, and the calendar isn't scroll-positioned to the working month. A forced layout reflow (e.g. resizing the window) makes the selection appear.

Expected Behavior

After the element is hidden and shown again - including when it has been relocated in the DOM between shows - the datetime should re-initialize and render its current selection, without needing an external reflow.

Steps to Reproduce

Self-contained repro (single HTML file, CDN, no build) is attached below. It keeps one <ion-datetime> and moves it between a visible container and a hidden pool (display: none) - a disconnect + connect on each open, calling reset('2026-07-10') each time, as an app would.

  1. Open → the 10th is rendered selected.
  2. Close, then Open again → the 10th is no longer rendered selected; the calendar is unstyled/stale, even though reset() ran and activeParts is correct.
  3. Open (with workaround) → clearing datetime-ready before showing restores the selection.

Important: do not inspect the element in devtools between steps, and don't resize the window - either forces a layout reflow that repaints the selection and masks the bug.

The stuck state is observable without forcing layout (a classList read doesn't reflow):

// element parked in the hidden pool after one show:
datetimeEl.classList.contains('datetime-ready'); // true - should be false while hidden

Code Reproduction URL

https://bolt.new/p/69113085

Everything is in index.html

Ionic Info

Ionic:

Ionic CLI : 7.2.1 (/opt/homebrew/lib/node_modules/@ionic/cli)

Capacitor:

Capacitor CLI : 8.3.4
@capacitor/android : 8.3.4
@capacitor/core : 8.3.4
@capacitor/ios : 8.3.4

Utility:

cordova-res : not installed globally
native-run : not installed globally

System:

NodeJS : v26.5.0 (/opt/homebrew/Cellar/node/26.5.0/bin/node)
npm : 11.17.0
OS : macOS Unknown

Additional Information

Root cause

The lazy-ready path added in 8.8.0 tracks readiness with hasBeenIntersecting / markReady() and gates re-initialization on the datetime-ready class. Line references are core/src/components/datetime/datetime.tsx at v8.8.14:

  • disconnectedCallback() (:1113) sets this.hasBeenIntersecting = false (:1119).
  • The hidden-state IntersectionObserver callback bails early on if (!this.hasBeenIntersecting) (:1250) before reaching this.el.classList.remove('datetime-ready') (:1267) - the guard added in fix(datetime): prevent hidden-state observer from tearing down ready class on initial entry #31108 (8.8.6).
  • So an element disconnected while it had been shown keeps datetime-ready set even though it's now hidden.
  • On the next show, connectedCallback() (:1106) → ensureReadyIfVisible() (:1109) early-returns on if (this.el.classList.contains('datetime-ready')) return (:1154), and markReady() (:1166) early-returns the same way (:1167). So initializeListeners() (:1170 → :1139) → initializeCalendarListener() (:1140, defined :902) never runs again. That listener scroll-positions the working month and lets the calendar paint, so the render is left stale.

In short: disconnectedCallback clears hasBeenIntersecting (:1119), which makes the hidden observer skip clearing datetime-ready (guard :1250 before :1267), which makes the next show skip re-initialization (:1154 / :1167).

Workaround

Remove the class immediately before re-showing so the ready path runs again:

datetimeEl.classList.remove('datetime-ready');

Suggested fix

Have the hidden-state observer (or disconnectedCallback) clear datetime-ready when the element is torn down, so a subsequent show goes through markReady() / initializeCalendarListener() again. The hasBeenIntersecting guard from #31108 shouldn't leave the ready class set on an element that has been disconnected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions