@@ -49,6 +49,7 @@ import {
4949 getPositionBasedSafeAreaConfig ,
5050 applySafeAreaOverrides ,
5151 clearSafeAreaOverrides ,
52+ getRootSafeAreaTop ,
5253 type ModalSafeAreaContext ,
5354} from './safe-area-utils' ;
5455import { setCardStatusBarDark , setCardStatusBarDefault } from './utils' ;
@@ -283,14 +284,35 @@ export class Modal implements ComponentInterface, OverlayInterface {
283284
284285 @Listen ( 'resize' , { target : 'window' } )
285286 onWindowResize ( ) {
286- // Only handle resize for iOS card modals when no custom animations are provided
287- if ( getIonMode ( this ) !== 'ios' || ! this . presentingElement || this . enterAnimation || this . leaveAnimation ) {
288- return ;
289- }
287+ if ( ! this . presented ) return ;
290288
291289 clearTimeout ( this . resizeTimeout ) ;
292290 this . resizeTimeout = setTimeout ( ( ) => {
293- this . handleViewTransition ( ) ;
291+ const context = this . getSafeAreaContext ( ) ;
292+
293+ // iOS card modals: handle portrait/landscape view transitions
294+ if ( context . isCardModal && ! this . enterAnimation && ! this . leaveAnimation ) {
295+ this . handleViewTransition ( ) ;
296+ }
297+
298+ // Sheet modals: re-compute the internal offset property since safe-area
299+ // values may change on device rotation (e.g., portrait notch vs landscape).
300+ if ( context . isSheetModal ) {
301+ this . updateSheetOffsetTop ( ) ;
302+ }
303+
304+ // Regular (non-sheet, non-card) modals: update safe-area overrides
305+ // since the viewport may have crossed the centered-dialog breakpoint.
306+ if ( ! context . isSheetModal && ! context . isCardModal ) {
307+ this . updateSafeAreaOverrides ( ) ;
308+
309+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
310+ if ( this . wrapperEl ) {
311+ this . wrapperEl . style . removeProperty ( 'height' ) ;
312+ this . wrapperEl . style . removeProperty ( 'padding-bottom' ) ;
313+ }
314+ this . applyFullscreenSafeArea ( ) ;
315+ }
294316 } , 50 ) ; // Debounce to avoid excessive calls during active resizing
295317 }
296318
@@ -770,8 +792,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
770792 if ( this . currentBreakpoint !== breakpoint ) {
771793 this . currentBreakpoint = breakpoint ;
772794 this . ionBreakpointDidChange . emit ( { breakpoint } ) ;
773- // Update safe-area overrides based on new position
774- this . updateSafeAreaOverrides ( ) ;
775795 }
776796 }
777797 ) ;
@@ -903,6 +923,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
903923 return false ;
904924 }
905925
926+ // Cancel any pending resize timeout to prevent stale updates during dismiss
927+ clearTimeout ( this . resizeTimeout ) ;
928+ this . resizeTimeout = undefined ;
929+
906930 /**
907931 * Because the canDismiss check below is async,
908932 * we need to claim a lock before the check happens,
@@ -1374,11 +1398,32 @@ export class Modal implements ComponentInterface, OverlayInterface {
13741398 /**
13751399 * Sets initial safe-area overrides before modal animation.
13761400 * Called in present() before animation starts.
1401+ *
1402+ * For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
1403+ * (an internal property) instead of --ion-safe-area-top. We resolve the
1404+ * root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
1405+ * the height calculation from --ion-safe-area-top (which is zeroed for
1406+ * sheets to prevent header content from getting double-offset padding).
13771407 */
13781408 private setInitialSafeAreaOverrides ( ) : void {
13791409 const context = this . getSafeAreaContext ( ) ;
13801410 const safeAreaConfig = getInitialSafeAreaConfig ( context ) ;
13811411 applySafeAreaOverrides ( this . el , safeAreaConfig ) ;
1412+
1413+ // Set the internal offset property with the resolved root safe-area-top value
1414+ if ( context . isSheetModal ) {
1415+ this . updateSheetOffsetTop ( ) ;
1416+ }
1417+ }
1418+
1419+ /**
1420+ * Resolves the current root --ion-safe-area-top value and sets the
1421+ * internal --ion-modal-offset-top property on the host element.
1422+ * Called on present and on resize (e.g., device rotation changes safe-area).
1423+ */
1424+ private updateSheetOffsetTop ( ) : void {
1425+ const safeAreaTop = getRootSafeAreaTop ( ) ;
1426+ this . el . style . setProperty ( '--ion-modal-offset-top' , `${ safeAreaTop } px` ) ;
13821427 }
13831428
13841429 /**
@@ -1389,19 +1434,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
13891434 const { wrapperEl, el } = this ;
13901435 const context = this . getSafeAreaContext ( ) ;
13911436
1392- // Sheet modals: the wrapper extends beyond the viewport and is translated
1393- // via breakpoint gestures, making getBoundingClientRect unreliable for
1394- // edge detection. Instead, use breakpoint value to determine top safe-area.
1395- if ( context . isSheetModal ) {
1396- const needsTopSafeArea = context . currentBreakpoint === 1 ;
1397- applySafeAreaOverrides ( el , {
1398- top : needsTopSafeArea ? 'inherit' : '0px' ,
1399- bottom : 'inherit' ,
1400- left : '0px' ,
1401- right : '0px' ,
1402- } ) ;
1403- return ;
1404- }
1437+ // Sheet modals: safe-area is fully determined at presentation time
1438+ // (top is always 0px, height is frozen). Nothing to update.
1439+ if ( context . isSheetModal ) return ;
14051440
14061441 // Card modals have fixed safe-area requirements set by initial prediction.
14071442 if ( context . isCardModal ) return ;
@@ -1454,6 +1489,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
14541489 private cleanupSafeAreaOverrides ( ) : void {
14551490 clearSafeAreaOverrides ( this . el ) ;
14561491
1492+ // Remove internal sheet offset property
1493+ this . el . style . removeProperty ( '--ion-modal-offset-top' ) ;
1494+
14571495 if ( this . wrapperEl ) {
14581496 this . wrapperEl . style . removeProperty ( 'height' ) ;
14591497 this . wrapperEl . style . removeProperty ( 'padding-bottom' ) ;
0 commit comments