@@ -108,8 +108,8 @@ export class Datetime implements ComponentInterface {
108108 private inputId = `ion-dt-${ datetimeIds ++ } ` ;
109109 private calendarBodyRef ?: HTMLElement ;
110110 private popoverRef ?: HTMLIonPopoverElement ;
111- private intersectionTrackerRef ?: HTMLElement ;
112111 private clearFocusVisible ?: ( ) => void ;
112+ private resizeObserver ?: ResizeObserver ;
113113 private parsedMinuteValues ?: number [ ] ;
114114 private parsedHourValues ?: number [ ] ;
115115 private parsedMonthValues ?: number [ ] ;
@@ -1077,6 +1077,10 @@ export class Datetime implements ComponentInterface {
10771077 this . clearFocusVisible ( ) ;
10781078 this . clearFocusVisible = undefined ;
10791079 }
1080+ if ( this . resizeObserver ) {
1081+ this . resizeObserver . disconnect ( ) ;
1082+ this . resizeObserver = undefined ;
1083+ }
10801084 }
10811085
10821086 /**
@@ -1101,114 +1105,77 @@ export class Datetime implements ComponentInterface {
11011105 this . initializeKeyboardListeners ( ) ;
11021106 }
11031107
1104- /**
1105- * TODO(FW-6931): Remove this fallback upon solving the root cause
1106- * Fallback to ensure the datetime becomes ready even if
1107- * IntersectionObserver never reports it as intersecting.
1108- *
1109- * This is primarily used in environments where the observer
1110- * might not fire as expected, such as when running under
1111- * synthetic tests that stub IntersectionObserver.
1112- */
1113- private ensureReadyIfVisible = ( ) => {
1114- if ( this . el . classList . contains ( 'datetime-ready' ) ) {
1115- return ;
1116- }
1117-
1118- const rect = this . el . getBoundingClientRect ( ) ;
1119- if ( rect . width === 0 || rect . height === 0 ) {
1120- return ;
1121- }
1122-
1123- this . initializeListeners ( ) ;
1124-
1125- writeTask ( ( ) => {
1126- this . el . classList . add ( 'datetime-ready' ) ;
1127- } ) ;
1128- } ;
1129-
11301108 componentDidLoad ( ) {
1131- const { el, intersectionTrackerRef } = this ;
1109+ const { el } = this ;
11321110
11331111 /**
11341112 * If a scrollable element is hidden using `display: none`,
11351113 * it will not have a scroll height meaning we cannot scroll elements
11361114 * into view. As a result, we will need to wait for the datetime to become
11371115 * visible if used inside of a modal or a popover otherwise the scrollable
11381116 * areas will not have the correct values snapped into place.
1117+ *
1118+ * FW-6931: We use ResizeObserver to detect when the element transitions
1119+ * between having dimensions (visible) and zero dimensions (hidden). This
1120+ * is more reliable than IntersectionObserver for detecting visibility
1121+ * changes, especially when the element is inside a modal or popover.
11391122 */
1140- const visibleCallback = ( entries : IntersectionObserverEntry [ ] ) => {
1141- const ev = entries [ 0 ] ;
1142- if ( ! ev . isIntersecting ) {
1143- return ;
1144- }
1123+ if ( typeof ResizeObserver !== 'undefined' ) {
1124+ this . resizeObserver = new ResizeObserver ( ( entries ) => {
1125+ const entry = entries [ 0 ] ;
1126+ const { width, height } = entry . contentRect ;
1127+ const isVisible = width > 0 && height > 0 ;
1128+ const isReady = el . classList . contains ( 'datetime-ready' ) ;
11451129
1146- this . initializeListeners ( ) ;
1130+ if ( isVisible && ! isReady ) {
1131+ this . initializeListeners ( ) ;
11471132
1148- /**
1149- * TODO FW-2793: Datetime needs a frame to ensure that it
1150- * can properly scroll contents into view. As a result
1151- * we hide the scrollable content until after that frame
1152- * so users do not see the content quickly shifting. The downside
1153- * is that the content will pop into view a frame after. Maybe there
1154- * is a better way to handle this?
1155- */
1156- writeTask ( ( ) => {
1157- this . el . classList . add ( 'datetime-ready' ) ;
1158- } ) ;
1159- } ;
1160- const visibleIO = new IntersectionObserver ( visibleCallback , { threshold : 0.01 , root : el } ) ;
1161-
1162- /**
1163- * Use raf to avoid a race condition between the component loading and
1164- * its display animation starting (such as when shown in a modal). This
1165- * could cause the datetime to start at a visibility of 0, erroneously
1166- * triggering the `hiddenIO` observer below.
1167- */
1168- raf ( ( ) => visibleIO ?. observe ( intersectionTrackerRef ! ) ) ;
1169-
1170- /**
1171- * TODO(FW-6931): Remove this fallback upon solving the root cause
1172- * Fallback: If IntersectionObserver never reports that the
1173- * datetime is visible but the host clearly has layout, ensure
1174- * we still initialize listeners and mark the component as ready.
1175- *
1176- * We schedule this after everything has had a chance to run.
1177- */
1178- setTimeout ( ( ) => {
1179- this . ensureReadyIfVisible ( ) ;
1180- } , 100 ) ;
1133+ /**
1134+ * TODO FW-2793: Datetime needs a frame to ensure that it
1135+ * can properly scroll contents into view. As a result
1136+ * we hide the scrollable content until after that frame
1137+ * so users do not see the content quickly shifting. The downside
1138+ * is that the content will pop into view a frame after. Maybe there
1139+ * is a better way to handle this?
1140+ */
1141+ writeTask ( ( ) => {
1142+ el . classList . add ( 'datetime-ready' ) ;
1143+ } ) ;
1144+ } else if ( ! isVisible && isReady ) {
1145+ /**
1146+ * Clean up listeners when hidden so we can properly
1147+ * reinitialize scroll positions on re-presentation.
1148+ */
1149+ this . destroyInteractionListeners ( ) ;
11811150
1182- /**
1183- * We need to clean up listeners when the datetime is hidden
1184- * in a popover/modal so that we can properly scroll containers
1185- * back into view if they are re-presented. When the datetime is hidden
1186- * the scroll areas have scroll widths/heights of 0px, so any snapping
1187- * we did originally has been lost.
1188- */
1189- const hiddenCallback = ( entries : IntersectionObserverEntry [ ] ) => {
1190- const ev = entries [ 0 ] ;
1191- if ( ev . isIntersecting ) {
1192- return ;
1193- }
1151+ /**
1152+ * Close month/year picker when hidden, otherwise
1153+ * it will be open when re-presented with a 0-height
1154+ * scroll area, showing the wrong month.
1155+ */
1156+ this . showMonthAndYear = false ;
11941157
1195- this . destroyInteractionListeners ( ) ;
1158+ writeTask ( ( ) => {
1159+ el . classList . remove ( 'datetime-ready' ) ;
1160+ } ) ;
1161+ }
1162+ } ) ;
11961163
11971164 /**
1198- * When datetime is hidden, we need to make sure that
1199- * the month/year picker is closed. Otherwise,
1200- * it will be open when the datetime re-appears
1201- * and the scroll area of the calendar grid will be 0.
1202- * As a result, the wrong month will be shown.
1165+ * Use raf to avoid a race condition between the component loading and
1166+ * its display animation starting (such as when shown in a modal).
1167+ */
1168+ raf ( ( ) => this . resizeObserver ?. observe ( el ) ) ;
1169+ } else {
1170+ /**
1171+ * Fallback for test environments where ResizeObserver is not available.
1172+ * Just mark as ready without initializing scroll/keyboard listeners
1173+ * since those also require browser APIs not available in Jest.
12031174 */
1204- this . showMonthAndYear = false ;
1205-
12061175 writeTask ( ( ) => {
1207- this . el . classList . remove ( 'datetime-ready' ) ;
1176+ el . classList . add ( 'datetime-ready' ) ;
12081177 } ) ;
1209- } ;
1210- const hiddenIO = new IntersectionObserver ( hiddenCallback , { threshold : 0 , root : el } ) ;
1211- raf ( ( ) => hiddenIO ?. observe ( intersectionTrackerRef ! ) ) ;
1178+ }
12121179
12131180 /**
12141181 * Datetime uses Ionic components that emit
@@ -2693,20 +2660,6 @@ export class Datetime implements ComponentInterface {
26932660 } ) ,
26942661 } }
26952662 >
2696- { /*
2697- WebKit has a quirk where IntersectionObserver callbacks are delayed until after
2698- an accelerated animation finishes if the "root" specified in the config is the
2699- browser viewport (the default behavior if "root" is not specified). This means
2700- that when presenting a datetime in a modal on iOS the calendar body appears
2701- blank until the modal animation finishes.
2702-
2703- We can work around this by observing .intersection-tracker and using the host
2704- (ion-datetime) as the "root". This allows the IO callback to fire the moment
2705- the datetime is visible. The .intersection-tracker element should not have
2706- dimensions or additional styles, and it should not be positioned absolutely
2707- otherwise the IO callback may fire at unexpected times.
2708- */ }
2709- < div class = "intersection-tracker" ref = { ( el ) => ( this . intersectionTrackerRef = el ) } > </ div >
27102663 { this . renderDatetime ( mode ) }
27112664 </ Host >
27122665 ) ;
0 commit comments