mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(datetime): ensure datetime is shown when intersection observer fails to report visibility (#30793)
Issue number: resolves #30706 --------- <!-- 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? Due to some recent unknown changes, the intersection observer for date time no longer reliably fires, especially in mobile views. ## What is the new behavior? In this PR, we're adding a visibility check after everything has had a chance to render to make sure we're setting up properly even if the intersection observer has failed to trigger for some reason. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information Since the intersection observer is being set up after a `raf`, it's possible something got introduced to make the initial setup slower for some reason, causing timing issues. I think we should do a more thorough investigation into the cause of this problem when we have more time. This PR also adds tests to verify the new fallback works properly. Current dev build: ``` 8.7.10-dev.11763478209.1d9c4cd8 ``` --------- Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1101,6 +1101,32 @@ export class Datetime implements ComponentInterface {
|
||||
this.initializeKeyboardListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO(FW-6931): Remove this fallback upon solving the root cause
|
||||
* 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;
|
||||
|
||||
@@ -1141,6 +1167,18 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
raf(() => visibleIO?.observe(intersectionTrackerRef!));
|
||||
|
||||
/**
|
||||
* TODO(FW-6931): Remove this fallback upon solving the root cause
|
||||
* 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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user