@@ -102,6 +102,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
102102 private cachedOriginalParent ?: HTMLElement ;
103103 // Cached ion-page ancestor for child route passthrough
104104 private cachedPageParent ?: HTMLElement | null ;
105+ // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
106+ private skipSafeAreaCoordinateDetection = false ;
105107
106108 lastFocus ?: HTMLElement ;
107109 animation ?: Animation ;
@@ -877,42 +879,61 @@ export class Modal implements ComponentInterface, OverlayInterface {
877879 const isCardModal = presentingElement !== undefined ;
878880 const isTablet = window . innerWidth >= 768 ;
879881
880- // Sheet modals: always touch bottom, top depends on breakpoint
882+ // Sheet modals always touch bottom edge, never top/left/right
881883 if ( isSheetModal ) {
882884 style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
883- // Don't override bottom - sheet always touches bottom
884885 style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
885886 style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
886887 return ;
887888 }
888889
889- // Card modals are inset from edges (rounded corners), no safe areas needed
890+ // Card modals are inset from all edges
890891 if ( isCardModal ) {
891- style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
892- style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
893- style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
894- style . setProperty ( '--ion-safe-area-right' , '0px' ) ;
892+ this . zeroAllSafeAreas ( ) ;
895893 return ;
896894 }
897895
898- // Phone modals are fullscreen, need all safe areas
896+ // Phone-sized fullscreen modals inherit safe areas and use wrapper padding
899897 if ( ! isTablet ) {
900- // Don't set any overrides - inherit from :root
898+ this . applyFullscreenSafeArea ( ) ;
901899 return ;
902900 }
903901
904- // Default tablet modal: centered dialog, no safe areas needed
905- // Check for fullscreen override via CSS custom properties
902+ // Check if tablet modal is fullscreen via CSS custom properties
906903 const computedStyle = getComputedStyle ( this . el ) ;
907904 const width = computedStyle . getPropertyValue ( '--width' ) . trim ( ) ;
908905 const height = computedStyle . getPropertyValue ( '--height' ) . trim ( ) ;
906+ const isFullscreen = width === '100%' && height === '100%' ;
909907
910- if ( width === '100%' && height === '100%' ) {
911- // Fullscreen modal - need safe areas, don't override
912- return ;
908+ if ( isFullscreen ) {
909+ this . applyFullscreenSafeArea ( ) ;
910+ } else {
911+ // Centered dialog doesn't touch edges
912+ this . zeroAllSafeAreas ( ) ;
913+ }
914+ }
915+
916+ /**
917+ * Applies safe-area handling for fullscreen modals.
918+ * Adds wrapper padding when no footer is present to prevent
919+ * content from overlapping system navigation areas.
920+ */
921+ private applyFullscreenSafeArea ( ) {
922+ this . skipSafeAreaCoordinateDetection = true ;
923+
924+ const hasFooter = this . el . querySelector ( 'ion-footer' ) !== null ;
925+ if ( ! hasFooter && this . wrapperEl ) {
926+ this . wrapperEl . style . setProperty ( 'padding-bottom' , 'var(--ion-safe-area-bottom, 0px)' ) ;
927+ this . wrapperEl . style . setProperty ( 'box-sizing' , 'border-box' ) ;
913928 }
929+ }
914930
915- // Centered dialog - zero out all safe areas
931+ /**
932+ * Sets all safe-area CSS variables to 0px for modals that
933+ * don't touch screen edges.
934+ */
935+ private zeroAllSafeAreas ( ) {
936+ const style = this . el . style ;
916937 style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
917938 style . setProperty ( '--ion-safe-area-bottom' , '0px' ) ;
918939 style . setProperty ( '--ion-safe-area-left' , '0px' ) ;
@@ -921,22 +942,27 @@ export class Modal implements ComponentInterface, OverlayInterface {
921942
922943 /**
923944 * Updates safe-area CSS variable overrides based on whether the modal
924- * is touching each edge of the viewport. This is called after animation
945+ * is touching each edge of the viewport. Called after animation
925946 * and during gestures to handle dynamic position changes.
926947 */
927948 private updateSafeAreaOverrides ( ) {
949+ if ( this . skipSafeAreaCoordinateDetection ) {
950+ return ;
951+ }
952+
928953 const wrapper = this . wrapperEl ;
929- if ( ! wrapper ) return ;
954+ if ( ! wrapper ) {
955+ return ;
956+ }
930957
931958 const rect = wrapper . getBoundingClientRect ( ) ;
932- const threshold = 2 ; // Account for subpixel rendering
959+ const threshold = 2 ;
933960
934961 const touchingTop = rect . top <= threshold ;
935962 const touchingBottom = rect . bottom >= window . innerHeight - threshold ;
936963 const touchingLeft = rect . left <= threshold ;
937964 const touchingRight = rect . right >= window . innerWidth - threshold ;
938965
939- // Remove override when touching edge (allow inheritance), set to 0 when not touching
940966 const style = this . el . style ;
941967 touchingTop ? style . removeProperty ( '--ion-safe-area-top' ) : style . setProperty ( '--ion-safe-area-top' , '0px' ) ;
942968 touchingBottom
@@ -1058,6 +1084,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
10581084 }
10591085 this . currentBreakpoint = undefined ;
10601086 this . animation = undefined ;
1087+ // Reset safe-area detection flag for potential re-presentation
1088+ this . skipSafeAreaCoordinateDetection = false ;
10611089
10621090 unlock ( ) ;
10631091
0 commit comments