fix(datetime): moving to overlay listeners for showing instead

This commit is contained in:
ShaneK
2025-12-12 08:41:17 -08:00
parent 12ef9345d6
commit a8772e8bd9

View File

@@ -118,6 +118,7 @@ export class Datetime implements ComponentInterface {
private destroyCalendarListener?: () => void; private destroyCalendarListener?: () => void;
private destroyKeyboardMO?: () => void; private destroyKeyboardMO?: () => void;
private destroyOverlayListeners?: () => void;
// TODO(FW-2832): types (DatetimeParts causes some errors that need untangling) // TODO(FW-2832): types (DatetimeParts causes some errors that need untangling)
private minParts?: any; private minParts?: any;
@@ -1081,6 +1082,10 @@ export class Datetime implements ComponentInterface {
this.resizeObserver.disconnect(); this.resizeObserver.disconnect();
this.resizeObserver = undefined; this.resizeObserver = undefined;
} }
if (this.destroyOverlayListeners) {
this.destroyOverlayListeners();
this.destroyOverlayListeners = undefined;
}
} }
/** /**
@@ -1106,41 +1111,21 @@ export class Datetime implements ComponentInterface {
} }
/** /**
* FW-6931: Fallback check for when ResizeObserver doesn't fire reliably * Sets up visibility detection for the datetime component.
* (e.g., WebKit during modal re-presentation). Called after element is *
* hidden to catch when it becomes visible again. * Uses multiple strategies to reliably detect when the datetime becomes
* visible, which is necessary for proper initialization of scrollable areas:
* 1. ResizeObserver - detects dimension changes
* 2. Overlay event listeners - for datetime inside modals/popovers
* 3. Polling fallback - for browsers where observers are unreliable (WebKit)
*/ */
private checkVisibilityFallback = () => { private initializeVisibilityObserver() {
const { el } = this;
if (el.classList.contains('datetime-ready')) {
return;
}
const rect = el.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.initializeListeners();
writeTask(() => {
el.classList.add('datetime-ready');
});
}
};
componentDidLoad() {
const { el } = this; const { el } = this;
/**
* If a scrollable element is hidden using `display: none`,
* it will not have a scroll height meaning we cannot scroll elements
* into view. As a result, we will need to wait for the datetime to become
* visible if used inside of a modal or a popover otherwise the scrollable
* areas will not have the correct values snapped into place.
*
* We use ResizeObserver to detect when the element transitions between
* having dimensions (visible) and zero dimensions (hidden). This is more
* reliable than IntersectionObserver for detecting visibility changes,
* especially when the element is inside a modal or popover.
*/
const markReady = () => { const markReady = () => {
if (el.classList.contains('datetime-ready')) {
return;
}
this.initializeListeners(); this.initializeListeners();
writeTask(() => { writeTask(() => {
el.classList.add('datetime-ready'); el.classList.add('datetime-ready');
@@ -1153,17 +1138,50 @@ export class Datetime implements ComponentInterface {
writeTask(() => { writeTask(() => {
el.classList.remove('datetime-ready'); el.classList.remove('datetime-ready');
}); });
/** startVisibilityPolling();
* Schedule fallback check for browsers where ResizeObserver
* doesn't fire reliably on re-presentation (e.g., WebKit).
*/
setTimeout(() => this.checkVisibilityFallback(), 100);
}; };
/**
* FW-6931: Poll for visibility as a fallback for browsers where
* ResizeObserver doesn't fire reliably (e.g., WebKit).
*/
const startVisibilityPolling = () => {
let pollCount = 0;
const poll = () => {
if (el.classList.contains('datetime-ready') || pollCount++ >= 60) {
return;
}
const { width, height } = el.getBoundingClientRect();
if (width > 0 && height > 0) {
markReady();
} else {
raf(poll);
}
};
raf(poll);
};
/**
* FW-6931: Listen for overlay present/dismiss events when datetime
* is inside a modal or popover.
*/
const parentOverlay = el.closest('ion-modal, ion-popover') as HTMLIonModalElement | HTMLIonPopoverElement | null;
if (parentOverlay) {
const handlePresent = () => markReady();
const handleDismiss = () => markHidden();
parentOverlay.addEventListener('didPresent', handlePresent);
parentOverlay.addEventListener('didDismiss', handleDismiss);
this.destroyOverlayListeners = () => {
parentOverlay.removeEventListener('didPresent', handlePresent);
parentOverlay.removeEventListener('didDismiss', handleDismiss);
};
}
if (typeof ResizeObserver !== 'undefined') { if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver((entries) => { this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0]; const { width, height } = entries[0].contentRect;
const { width, height } = entry.contentRect;
const isVisible = width > 0 && height > 0; const isVisible = width > 0 && height > 0;
const isReady = el.classList.contains('datetime-ready'); const isReady = el.classList.contains('datetime-ready');
@@ -1174,27 +1192,19 @@ export class Datetime implements ComponentInterface {
} }
}); });
/** // Use raf to avoid race condition with modal/popover animations
* Use raf to avoid a race condition between the component loading and
* its display animation starting (such as when shown in a modal).
*/
raf(() => this.resizeObserver?.observe(el)); raf(() => this.resizeObserver?.observe(el));
startVisibilityPolling();
/**
* Fallback for initial presentation in case ResizeObserver
* doesn't fire reliably (e.g., WebKit).
*/
setTimeout(() => this.checkVisibilityFallback(), 100);
} else { } else {
/** // Test environment fallback - mark ready immediately
* Fallback for test environments where ResizeObserver is not available.
* Just mark as ready without initializing scroll/keyboard listeners
* since those also require browser APIs not available in Jest.
*/
writeTask(() => { writeTask(() => {
el.classList.add('datetime-ready'); el.classList.add('datetime-ready');
}); });
} }
}
componentDidLoad() {
this.initializeVisibilityObserver();
/** /**
* Datetime uses Ionic components that emit * Datetime uses Ionic components that emit