Skip to content
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import { expect } from '@playwright/test';
import type { EventSpy } from '@utils/test/playwright';
import { configs, test } from '@utils/test/playwright';

/**
* This behavior does not vary across directions.
Expand Down Expand Up @@ -176,5 +176,34 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await ionModalDidPresent.next();
await expect(datetime).toBeVisible();
});
test('should set datetime ready state and keep calendar interactive when reopening modal', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30706',
});

const openAndInteract = async () => {
await page.click('#date-button');
await ionModalDidPresent.next();

await page.locator('ion-datetime.datetime-ready').waitFor();

const calendarBody = datetime.locator('.calendar-body');
await expect(calendarBody).toBeVisible();
};

await openAndInteract();

const firstEnabledDay = datetime.locator('.calendar-day:not([disabled])').first();
await firstEnabledDay.click();
await page.waitForChanges();

await modal.evaluate((el: HTMLIonModalElement) => el.dismiss());
await ionModalDidDismiss.next();

await openAndInteract();
});
});
});
36 changes: 36 additions & 0 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,31 @@ export class Datetime implements ComponentInterface {
this.initializeKeyboardListeners();
}

/**
* Fallback to ensure the datetime becomes ready even if
* IntersectionObserver never reports it as intersecting.
*
* This is primarily used in environments where the observer
* might not fire as expected, such as when running under
* synthetic tests that stub IntersectionObserver.
*/
private ensureReadyIfVisible = () => {
if (this.el.classList.contains('datetime-ready')) {
return;
}

const rect = this.el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return;
}

this.initializeListeners();

writeTask(() => {
this.el.classList.add('datetime-ready');
});
};

componentDidLoad() {
const { el, intersectionTrackerRef } = this;

Expand Down Expand Up @@ -1141,6 +1166,17 @@ export class Datetime implements ComponentInterface {
*/
raf(() => visibleIO?.observe(intersectionTrackerRef!));

/**
* Fallback: If IntersectionObserver never reports that the
* datetime is visible but the host clearly has layout, ensure
* we still initialize listeners and mark the component as ready.
*
* We schedule this after everything has had a chance to run.
*/
setTimeout(() => {
this.ensureReadyIfVisible();
}, 100);

/**
* We need to clean up listeners when the datetime is hidden
* in a popover/modal so that we can properly scroll containers
Expand Down
55 changes: 55 additions & 0 deletions core/src/components/datetime/test/basic/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,61 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
});
});

/**
* Synthetic IntersectionObserver fallback behavior.
*
* This test stubs IntersectionObserver so that the callback
* never reports an intersecting entry. The datetime should
* still become ready via its internal fallback logic.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('datetime: IO fallback'), () => {
test('should become ready even if IntersectionObserver never reports visible', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30706',
});

await page.addInitScript(() => {
const OriginalIO = window.IntersectionObserver;
(window as any).IntersectionObserver = function (callback: any, options: any) {
const instance = new OriginalIO(() => {}, options);
const originalObserve = instance.observe.bind(instance);

instance.observe = (target: Element) => {
originalObserve(target);
callback([
{
isIntersecting: false,
target,
} as IntersectionObserverEntry,
]);
};

return instance;
} as any;
});

await page.setContent(
`
<ion-datetime value="2022-05-03"></ion-datetime>
`,
config
);

const datetime = page.locator('ion-datetime');

// Give the fallback a short amount of time to run
await page.waitForTimeout(100);

await expect(datetime).toHaveClass(/datetime-ready/);

const calendarBody = datetime.locator('.calendar-body');
await expect(calendarBody).toBeVisible();
});
});
});

/**
* We are setting RTL on the component
* instead, so we don't need to test
Expand Down
Loading