@@ -118,6 +118,7 @@ export class Datetime implements ComponentInterface {
118118
119119 private destroyCalendarListener ?: ( ) => void ;
120120 private destroyKeyboardMO ?: ( ) => void ;
121+ private destroyOverlayListeners ?: ( ) => void ;
121122
122123 // TODO(FW-2832): types (DatetimeParts causes some errors that need untangling)
123124 private minParts ?: any ;
@@ -1081,6 +1082,10 @@ export class Datetime implements ComponentInterface {
10811082 this . resizeObserver . disconnect ( ) ;
10821083 this . resizeObserver = undefined ;
10831084 }
1085+ if ( this . destroyOverlayListeners ) {
1086+ this . destroyOverlayListeners ( ) ;
1087+ this . destroyOverlayListeners = undefined ;
1088+ }
10841089 }
10851090
10861091 /**
@@ -1106,41 +1111,21 @@ export class Datetime implements ComponentInterface {
11061111 }
11071112
11081113 /**
1109- * FW-6931: Fallback check for when ResizeObserver doesn't fire reliably
1110- * (e.g., WebKit during modal re-presentation). Called after element is
1111- * hidden to catch when it becomes visible again.
1114+ * Sets up visibility detection for the datetime component.
1115+ *
1116+ * Uses multiple strategies to reliably detect when the datetime becomes
1117+ * visible, which is necessary for proper initialization of scrollable areas:
1118+ * 1. ResizeObserver - detects dimension changes
1119+ * 2. Overlay event listeners - for datetime inside modals/popovers
1120+ * 3. Polling fallback - for browsers where observers are unreliable (WebKit)
11121121 */
1113- private checkVisibilityFallback = ( ) => {
1114- const { el } = this ;
1115- if ( el . classList . contains ( 'datetime-ready' ) ) {
1116- return ;
1117- }
1118-
1119- const rect = el . getBoundingClientRect ( ) ;
1120- if ( rect . width > 0 && rect . height > 0 ) {
1121- this . initializeListeners ( ) ;
1122- writeTask ( ( ) => {
1123- el . classList . add ( 'datetime-ready' ) ;
1124- } ) ;
1125- }
1126- } ;
1127-
1128- componentDidLoad ( ) {
1122+ private initializeVisibilityObserver ( ) {
11291123 const { el } = this ;
11301124
1131- /**
1132- * If a scrollable element is hidden using `display: none`,
1133- * it will not have a scroll height meaning we cannot scroll elements
1134- * into view. As a result, we will need to wait for the datetime to become
1135- * visible if used inside of a modal or a popover otherwise the scrollable
1136- * areas will not have the correct values snapped into place.
1137- *
1138- * We use ResizeObserver to detect when the element transitions between
1139- * having dimensions (visible) and zero dimensions (hidden). This is more
1140- * reliable than IntersectionObserver for detecting visibility changes,
1141- * especially when the element is inside a modal or popover.
1142- */
11431125 const markReady = ( ) => {
1126+ if ( el . classList . contains ( 'datetime-ready' ) ) {
1127+ return ;
1128+ }
11441129 this . initializeListeners ( ) ;
11451130 writeTask ( ( ) => {
11461131 el . classList . add ( 'datetime-ready' ) ;
@@ -1153,17 +1138,50 @@ export class Datetime implements ComponentInterface {
11531138 writeTask ( ( ) => {
11541139 el . classList . remove ( 'datetime-ready' ) ;
11551140 } ) ;
1156- /**
1157- * Schedule fallback check for browsers where ResizeObserver
1158- * doesn't fire reliably on re-presentation (e.g., WebKit).
1159- */
1160- setTimeout ( ( ) => this . checkVisibilityFallback ( ) , 100 ) ;
1141+ startVisibilityPolling ( ) ;
1142+ } ;
1143+
1144+ /**
1145+ * FW-6931: Poll for visibility as a fallback for browsers where
1146+ * ResizeObserver doesn't fire reliably (e.g., WebKit).
1147+ */
1148+ const startVisibilityPolling = ( ) => {
1149+ let pollCount = 0 ;
1150+ const poll = ( ) => {
1151+ if ( el . classList . contains ( 'datetime-ready' ) || pollCount ++ >= 60 ) {
1152+ return ;
1153+ }
1154+ const { width, height } = el . getBoundingClientRect ( ) ;
1155+ if ( width > 0 && height > 0 ) {
1156+ markReady ( ) ;
1157+ } else {
1158+ raf ( poll ) ;
1159+ }
1160+ } ;
1161+ raf ( poll ) ;
11611162 } ;
11621163
1164+ /**
1165+ * FW-6931: Listen for overlay present/dismiss events when datetime
1166+ * is inside a modal or popover.
1167+ */
1168+ const parentOverlay = el . closest ( 'ion-modal, ion-popover' ) as HTMLIonModalElement | HTMLIonPopoverElement | null ;
1169+ if ( parentOverlay ) {
1170+ const handlePresent = ( ) => markReady ( ) ;
1171+ const handleDismiss = ( ) => markHidden ( ) ;
1172+
1173+ parentOverlay . addEventListener ( 'didPresent' , handlePresent ) ;
1174+ parentOverlay . addEventListener ( 'didDismiss' , handleDismiss ) ;
1175+
1176+ this . destroyOverlayListeners = ( ) => {
1177+ parentOverlay . removeEventListener ( 'didPresent' , handlePresent ) ;
1178+ parentOverlay . removeEventListener ( 'didDismiss' , handleDismiss ) ;
1179+ } ;
1180+ }
1181+
11631182 if ( typeof ResizeObserver !== 'undefined' ) {
11641183 this . resizeObserver = new ResizeObserver ( ( entries ) => {
1165- const entry = entries [ 0 ] ;
1166- const { width, height } = entry . contentRect ;
1184+ const { width, height } = entries [ 0 ] . contentRect ;
11671185 const isVisible = width > 0 && height > 0 ;
11681186 const isReady = el . classList . contains ( 'datetime-ready' ) ;
11691187
@@ -1174,27 +1192,19 @@ export class Datetime implements ComponentInterface {
11741192 }
11751193 } ) ;
11761194
1177- /**
1178- * Use raf to avoid a race condition between the component loading and
1179- * its display animation starting (such as when shown in a modal).
1180- */
1195+ // Use raf to avoid race condition with modal/popover animations
11811196 raf ( ( ) => this . resizeObserver ?. observe ( el ) ) ;
1182-
1183- /**
1184- * Fallback for initial presentation in case ResizeObserver
1185- * doesn't fire reliably (e.g., WebKit).
1186- */
1187- setTimeout ( ( ) => this . checkVisibilityFallback ( ) , 100 ) ;
1197+ startVisibilityPolling ( ) ;
11881198 } else {
1189- /**
1190- * Fallback for test environments where ResizeObserver is not available.
1191- * Just mark as ready without initializing scroll/keyboard listeners
1192- * since those also require browser APIs not available in Jest.
1193- */
1199+ // Test environment fallback - mark ready immediately
11941200 writeTask ( ( ) => {
11951201 el . classList . add ( 'datetime-ready' ) ;
11961202 } ) ;
11971203 }
1204+ }
1205+
1206+ componentDidLoad ( ) {
1207+ this . initializeVisibilityObserver ( ) ;
11981208
11991209 /**
12001210 * Datetime uses Ionic components that emit
0 commit comments