@@ -98,12 +98,18 @@ export class Modal implements ComponentInterface, OverlayInterface {
9898
9999 // Mutation observer to watch for parent removal
100100 private parentRemovalObserver ?: MutationObserver ;
101+ // Watches for dynamic footer additions/removals to update safe-area padding
102+ private footerObserver ?: MutationObserver ;
101103 // Cached original parent from before modal is moved to body during presentation
102104 private cachedOriginalParent ?: HTMLElement ;
103105 // Cached ion-page ancestor for child route passthrough
104106 private cachedPageParent ?: HTMLElement | null ;
105107 // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
106108 private skipSafeAreaCoordinateDetection = false ;
109+ // Cached safe-area values to avoid getComputedStyle calls during gestures
110+ private cachedSafeAreas ?: { top : number ; bottom : number ; left : number ; right : number } ;
111+ // Track previous safe-area state to avoid redundant DOM writes
112+ private prevSafeAreaState = { top : false , bottom : false , left : false , right : false } ;
107113
108114 lastFocus ?: HTMLElement ;
109115 animation ?: Animation ;
@@ -278,7 +284,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
278284
279285 @Listen ( 'resize' , { target : 'window' } )
280286 onWindowResize ( ) {
281- // Update safe-area overrides for all modal types on resize
287+ // Invalidate safe-area cache on resize (device rotation may change values)
288+ this . cachedSafeAreas = undefined ;
282289 this . updateSafeAreaOverrides ( ) ;
283290
284291 // Only handle view transition for iOS card modals when no custom animations are provided
@@ -931,9 +938,27 @@ export class Modal implements ComponentInterface, OverlayInterface {
931938 */
932939 private applyFullscreenSafeArea ( ) {
933940 this . skipSafeAreaCoordinateDetection = true ;
941+ this . updateFooterPadding ( ) ;
942+
943+ // Watch for dynamic footer additions/removals (e.g., async data loading)
944+ if ( ! this . footerObserver ) {
945+ this . footerObserver = new MutationObserver ( ( ) => this . updateFooterPadding ( ) ) ;
946+ this . footerObserver . observe ( this . el , { childList : true , subtree : true } ) ;
947+ }
948+ }
949+
950+ /**
951+ * Updates wrapper padding based on footer presence.
952+ * Called initially and when footer is dynamically added/removed.
953+ */
954+ private updateFooterPadding ( ) {
955+ if ( ! this . wrapperEl ) return ;
934956
935957 const hasFooter = this . el . querySelector ( 'ion-footer' ) !== null ;
936- if ( ! hasFooter && this . wrapperEl ) {
958+ if ( hasFooter ) {
959+ this . wrapperEl . style . removeProperty ( 'padding-bottom' ) ;
960+ this . wrapperEl . style . removeProperty ( 'box-sizing' ) ;
961+ } else {
937962 this . wrapperEl . style . setProperty ( 'padding-bottom' , 'var(--ion-safe-area-bottom, 0px)' ) ;
938963 this . wrapperEl . style . setProperty ( 'box-sizing' , 'border-box' ) ;
939964 }
@@ -953,22 +978,27 @@ export class Modal implements ComponentInterface, OverlayInterface {
953978
954979 /**
955980 * Gets the root safe-area values from the document element.
956- * These represent the actual device safe areas before any overlay overrides .
981+ * Uses cached values during gestures to avoid getComputedStyle calls .
957982 */
958- private getRootSafeAreaValues ( ) : { top : number ; bottom : number ; left : number ; right : number } {
959- const rootStyle = getComputedStyle ( document . documentElement ) ;
960- return {
961- top : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-top' ) ) || 0 ,
962- bottom : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-bottom' ) ) || 0 ,
963- left : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-left' ) ) || 0 ,
964- right : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-right' ) ) || 0 ,
965- } ;
983+ private getSafeAreaValues ( ) : { top : number ; bottom : number ; left : number ; right : number } {
984+ if ( ! this . cachedSafeAreas ) {
985+ const rootStyle = getComputedStyle ( document . documentElement ) ;
986+ this . cachedSafeAreas = {
987+ top : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-top' ) ) || 0 ,
988+ bottom : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-bottom' ) ) || 0 ,
989+ left : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-left' ) ) || 0 ,
990+ right : parseFloat ( rootStyle . getPropertyValue ( '--ion-safe-area-right' ) ) || 0 ,
991+ } ;
992+ }
993+ return this . cachedSafeAreas ;
966994 }
967995
968996 /**
969997 * Updates safe-area CSS variable overrides based on whether the modal
970998 * extends into each safe-area region. Called after animation
971999 * and during gestures to handle dynamic position changes.
1000+ *
1001+ * Optimized to avoid redundant DOM writes by tracking previous state.
9721002 */
9731003 private updateSafeAreaOverrides ( ) {
9741004 if ( this . skipSafeAreaCoordinateDetection ) {
@@ -981,22 +1011,37 @@ export class Modal implements ComponentInterface, OverlayInterface {
9811011 }
9821012
9831013 const rect = wrapper . getBoundingClientRect ( ) ;
984- const safeAreas = this . getRootSafeAreaValues ( ) ;
1014+ const safeAreas = this . getSafeAreaValues ( ) ;
9851015
9861016 const extendsIntoTop = rect . top < safeAreas . top ;
9871017 const extendsIntoBottom = rect . bottom > window . innerHeight - safeAreas . bottom ;
9881018 const extendsIntoLeft = rect . left < safeAreas . left ;
9891019 const extendsIntoRight = rect . right > window . innerWidth - safeAreas . right ;
9901020
1021+ // Only update DOM when state actually changes
1022+ const prev = this . prevSafeAreaState ;
9911023 const style = this . el . style ;
992- extendsIntoTop ? style . removeProperty ( '--ion-safe-area-top' ) : style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
993- extendsIntoBottom
994- ? style . removeProperty ( '--ion-safe-area-bottom' )
995- : style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
996- extendsIntoLeft ? style . removeProperty ( '--ion-safe-area-left' ) : style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
997- extendsIntoRight
998- ? style . removeProperty ( '--ion-safe-area-right' )
999- : style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
1024+
1025+ if ( extendsIntoTop !== prev . top ) {
1026+ extendsIntoTop ? style . removeProperty ( '--ion-safe-area-top' ) : style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
1027+ prev . top = extendsIntoTop ;
1028+ }
1029+ if ( extendsIntoBottom !== prev . bottom ) {
1030+ extendsIntoBottom
1031+ ? style . removeProperty ( '--ion-safe-area-bottom' )
1032+ : style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
1033+ prev . bottom = extendsIntoBottom ;
1034+ }
1035+ if ( extendsIntoLeft !== prev . left ) {
1036+ extendsIntoLeft ? style . removeProperty ( '--ion-safe-area-left' ) : style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
1037+ prev . left = extendsIntoLeft ;
1038+ }
1039+ if ( extendsIntoRight !== prev . right ) {
1040+ extendsIntoRight
1041+ ? style . removeProperty ( '--ion-safe-area-right' )
1042+ : style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
1043+ prev . right = extendsIntoRight ;
1044+ }
10001045 }
10011046
10021047 private sheetOnDismiss ( ) {
@@ -1111,8 +1156,23 @@ export class Modal implements ComponentInterface, OverlayInterface {
11111156 }
11121157 this . currentBreakpoint = undefined ;
11131158 this . animation = undefined ;
1114- // Reset safe-area detection flag for potential re-presentation
1159+ // Reset safe-area state for potential re-presentation
11151160 this . skipSafeAreaCoordinateDetection = false ;
1161+ this . cachedSafeAreas = undefined ;
1162+ this . prevSafeAreaState = { top : false , bottom : false , left : false , right : false } ;
1163+ this . footerObserver ?. disconnect ( ) ;
1164+ this . footerObserver = undefined ;
1165+ // Clear styles that may have been set for safe-area handling
1166+ if ( this . wrapperEl ) {
1167+ this . wrapperEl . style . removeProperty ( 'padding-bottom' ) ;
1168+ this . wrapperEl . style . removeProperty ( 'box-sizing' ) ;
1169+ }
1170+ // Clear safe-area CSS variable overrides
1171+ const style = this . el . style ;
1172+ style . removeProperty ( '--ion-safe-area-top' ) ;
1173+ style . removeProperty ( '--ion-safe-area-bottom' ) ;
1174+ style . removeProperty ( '--ion-safe-area-left' ) ;
1175+ style . removeProperty ( '--ion-safe-area-right' ) ;
11161176
11171177 unlock ( ) ;
11181178
0 commit comments