diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Chrome-linux.png index 51197814d40..c6c1a2d880f 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Firefox-linux.png index d6978bd6ab2..ff6cce8fe9f 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Safari-linux.png index 7b6912a77b6..6755f318f78 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Chrome-linux.png index 9bdb2a0050d..41c48a001a7 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Firefox-linux.png index 2160bf71c71..946763fa06b 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Safari-linux.png index e18e09840ac..9b5d9e71eb6 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-action-sheet-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Chrome-linux.png index 2925d01cc40..39edcfcfb0f 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Firefox-linux.png index 625d61957f4..5d00b4c0a20 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Safari-linux.png index 636bbd74326..c6ba890db5d 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Chrome-linux.png index 6bf467070f7..21aae4987c5 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Firefox-linux.png index 31175c53c52..cd84a1e4519 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Safari-linux.png index d1e17793401..9ec7a229728 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-menu-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Chrome-linux.png index a72dc772243..e97a03df44d 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Firefox-linux.png index 6796d9a484f..adb36b2796f 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Safari-linux.png index 2d747790320..edcd8a84e14 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Chrome-linux.png index dd8e90750f3..8f969d0ba43 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Firefox-linux.png index eaa50d6e5fb..265eef0f687 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Safari-linux.png index a291fe4c078..9f2416e4f12 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-picker-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Chrome-linux.png index 2217a13a189..7fdc24274c2 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Firefox-linux.png index 6d49226ac1d..aa87aa60c88 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Safari-linux.png index 3619f94220f..067b270569c 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Chrome-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Chrome-linux.png index 69df91ae522..46f2c980b7e 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Firefox-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Firefox-linux.png index e8afb2df67d..f620cdf642c 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Safari-linux.png b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Safari-linux.png index fd6120b1681..a144e9df1a3 100644 Binary files a/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Safari-linux.png and b/core/src/components/app/test/safe-area/app.e2e.ts-snapshots/app-toast-diff-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Chrome-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Chrome-linux.png index aee611bf378..13d07317bca 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Firefox-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Firefox-linux.png index 9f3d30bfff5..770a9fa4bde 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Safari-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Safari-linux.png index 3a851565133..4f503d01279 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Safari-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Chrome-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Chrome-linux.png index 9660aeeadcd..097abe9a87a 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Firefox-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Firefox-linux.png index 6c1d1ee97b7..d64ca070c3c 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Safari-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Safari-linux.png index 35e0df2fb84..7fa31180ae1 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Safari-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-end-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Chrome-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Chrome-linux.png index 9660aeeadcd..097abe9a87a 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Firefox-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Firefox-linux.png index 6c1d1ee97b7..d64ca070c3c 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Safari-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Safari-linux.png index 35e0df2fb84..7fa31180ae1 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Safari-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Chrome-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Chrome-linux.png index aee611bf378..13d07317bca 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Firefox-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Firefox-linux.png index 9f3d30bfff5..770a9fa4bde 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Safari-linux.png b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Safari-linux.png index 3a851565133..4f503d01279 100644 Binary files a/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Safari-linux.png and b/core/src/components/fab/test/safe-area/fab.e2e.ts-snapshots/fab-safe-area-horizontal-start-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png index d23db0f8082..847203b959b 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png index dec6cbe80d4..7505285ca78 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png index b526f0f0a61..14c96b40bf9 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png index a690482d70c..f13e9e1f2e6 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png index 78668e8a1c7..2f1193a4a01 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png index a2d9893fc26..5ae9984cfe3 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png index 9303d7426af..0f76f7185e0 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png index f364b05c25c..c4139d9ef55 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png index a7168d36400..ba2ceae4263 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png index 58c28a228f8..a0a915efe72 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png index 927087d578b..401f304853f 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png index 221ed10fc32..949acf332de 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-end-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png index 30038ec4c37..ae939a0efd6 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png index 3d9d4b61321..d059cae1394 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png index 36580ae58cb..8069aae42a6 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png index 6c5d9946a2e..b02c9047c8f 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png index 3cb029908fa..7d1cb3fb061 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png index 52e0ca5ceb0..62b847347e6 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-left-notch-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png index 1f9094154ab..3420ea7d779 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png index 4e89bfd3183..dfb2430c1b4 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png index c289cff38b8..e6d1c1b2789 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png index a9689438292..c73aed5103f 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png index f7f75c751fb..e0ccec89203 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png index a8ebda13d1e..4843b6da194 100644 Binary files a/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png and b/core/src/components/menu/test/safe-area/menu.e2e.ts-snapshots/menu-start-safe-area-right-notch-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index afac8f3d3ed..a9f7855e4d4 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -52,7 +52,8 @@ export const createSheetGesture = ( expandToScroll: boolean, getCurrentBreakpoint: () => number, onDismiss: () => void, - onBreakpointChange: (breakpoint: number) => void + onBreakpointChange: (breakpoint: number) => void, + onGestureMove?: () => void ) => { // Defaults for the sheet swipe animation const defaultBackdrop = [ @@ -423,6 +424,9 @@ export const createSheetGesture = ( offset = clamp(0.0001, processedStep, maxStep); animation.progressStep(offset); + + // Notify modal of position change for safe-area updates + onGestureMove?.(); }; const onEnd = (detail: GestureDetail) => { diff --git a/core/src/components/modal/gestures/swipe-to-close.ts b/core/src/components/modal/gestures/swipe-to-close.ts index 17ec454ff15..c81a6a6ba21 100644 --- a/core/src/components/modal/gestures/swipe-to-close.ts +++ b/core/src/components/modal/gestures/swipe-to-close.ts @@ -20,7 +20,8 @@ export const createSwipeToCloseGesture = ( el: HTMLIonModalElement, animation: Animation, statusBarStyle: StatusBarStyle, - onDismiss: () => void + onDismiss: () => void, + onGestureMove?: () => void ) => { /** * The step value at which a card modal @@ -199,6 +200,9 @@ export const createSwipeToCloseGesture = ( animation.progressStep(clampedStep); + // Notify modal of position change for safe-area updates + onGestureMove?.(); + /** * When swiping down half way, the status bar style * should be reset to its default value. diff --git a/core/src/components/modal/modal.scss b/core/src/components/modal/modal.scss index 7c5ec7916fe..ac4cb533b48 100644 --- a/core/src/components/modal/modal.scss +++ b/core/src/components/modal/modal.scss @@ -94,10 +94,6 @@ ion-backdrop { :host { --width: #{$modal-inset-width}; --height: #{$modal-inset-height-small}; - --ion-safe-area-top: 0px; - --ion-safe-area-bottom: 0px; - --ion-safe-area-right: 0px; - --ion-safe-area-left: 0px; } } diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index a96d59c8e9f..bf57c36f073 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h, writeTask } from '@stencil/core'; +import { win } from '@utils/browser'; import { findIonContent, printIonContentErrorMsg } from '@utils/content'; import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate'; import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from '@utils/helpers'; @@ -98,10 +99,18 @@ export class Modal implements ComponentInterface, OverlayInterface { // Mutation observer to watch for parent removal private parentRemovalObserver?: MutationObserver; + // Watches for dynamic footer additions/removals to update safe-area padding + private footerObserver?: MutationObserver; // Cached original parent from before modal is moved to body during presentation private cachedOriginalParent?: HTMLElement; // Cached ion-page ancestor for child route passthrough private cachedPageParent?: HTMLElement | null; + // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals) + private skipSafeAreaCoordinateDetection = false; + // Cached safe-area values to avoid getComputedStyle calls during gestures + private cachedSafeAreas?: { top: number; bottom: number; left: number; right: number }; + // Track previous safe-area state to avoid redundant DOM writes + private prevSafeAreaState = { top: false, bottom: false, left: false, right: false }; lastFocus?: HTMLElement; animation?: Animation; @@ -276,7 +285,11 @@ export class Modal implements ComponentInterface, OverlayInterface { @Listen('resize', { target: 'window' }) onWindowResize() { - // Only handle resize for iOS card modals when no custom animations are provided + // Invalidate safe-area cache on resize (device rotation may change values) + this.cachedSafeAreas = undefined; + this.updateSafeAreaOverrides(); + + // Only handle view transition for iOS card modals when no custom animations are provided if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) { return; } @@ -406,6 +419,8 @@ export class Modal implements ComponentInterface, OverlayInterface { this.triggerController.removeClickListener(); this.cleanupViewTransitionListener(); this.cleanupParentRemovalObserver(); + // Reset safe-area state to handle removal without dismiss (e.g., framework unmount) + this.resetSafeAreaState(); } componentWillLoad() { @@ -592,6 +607,9 @@ export class Modal implements ComponentInterface, OverlayInterface { await waitForMount(); } + // Predict safe-area needs based on modal configuration to avoid visual snap + this.setInitialSafeAreaOverrides(presentingElement); + writeTask(() => this.el.classList.add('show-modal')); const hasCardModal = presentingElement !== undefined; @@ -659,6 +677,9 @@ export class Modal implements ComponentInterface, OverlayInterface { this.initSwipeToClose(); } + // Now that animation is complete, update safe-area based on actual position + this.updateSafeAreaOverrides(); + // Initialize view transition listener for iOS card modals this.initViewTransitionListener(); @@ -692,33 +713,39 @@ export class Modal implements ComponentInterface, OverlayInterface { const statusBarStyle = this.statusBarStyle ?? StatusBarStyle.Default; - this.gesture = createSwipeToCloseGesture(el, ani, statusBarStyle, () => { - /** - * While the gesture animation is finishing - * it is possible for a user to tap the backdrop. - * This would result in the dismiss animation - * being played again. Typically this is avoided - * by setting `presented = false` on the overlay - * component; however, we cannot do that here as - * that would prevent the element from being - * removed from the DOM. - */ - this.gestureAnimationDismissing = true; - - /** - * Reset the status bar style as the dismiss animation - * starts otherwise the status bar will be the wrong - * color for the duration of the dismiss animation. - * The dismiss method does this as well, but - * in this case it's only called once the animation - * has finished. - */ - setCardStatusBarDefault(this.statusBarStyle); - this.animation!.onFinish(async () => { - await this.dismiss(undefined, GESTURE); - this.gestureAnimationDismissing = false; - }); - }); + this.gesture = createSwipeToCloseGesture( + el, + ani, + statusBarStyle, + () => { + /** + * While the gesture animation is finishing + * it is possible for a user to tap the backdrop. + * This would result in the dismiss animation + * being played again. Typically this is avoided + * by setting `presented = false` on the overlay + * component; however, we cannot do that here as + * that would prevent the element from being + * removed from the DOM. + */ + this.gestureAnimationDismissing = true; + + /** + * Reset the status bar style as the dismiss animation + * starts otherwise the status bar will be the wrong + * color for the duration of the dismiss animation. + * The dismiss method does this as well, but + * in this case it's only called once the animation + * has finished. + */ + setCardStatusBarDefault(this.statusBarStyle); + this.animation!.onFinish(async () => { + await this.dismiss(undefined, GESTURE); + this.gestureAnimationDismissing = false; + }); + }, + () => this.updateSafeAreaOverrides() + ); this.gesture.enable(true); } @@ -755,7 +782,9 @@ export class Modal implements ComponentInterface, OverlayInterface { this.currentBreakpoint = breakpoint; this.ionBreakpointDidChange.emit({ breakpoint }); } - } + this.updateSafeAreaOverrides(); + }, + () => this.updateSafeAreaOverrides() ); this.gesture = gesture; @@ -849,6 +878,203 @@ export class Modal implements ComponentInterface, OverlayInterface { this.cachedPageParent = undefined; } + /** + * Sets initial safe-area overrides based on modal configuration before + * the modal becomes visible. This predicts whether the modal will touch + * screen edges to avoid a visual snap after animation completes. + */ + private setInitialSafeAreaOverrides(presentingElement: HTMLElement | undefined) { + const style = this.el.style; + const mode = getIonMode(this); + const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined; + // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored + const isCardModal = presentingElement !== undefined && mode === 'ios'; + const isTablet = window.innerWidth >= 768; + + // Sheet modals always touch bottom edge, never top/left/right + if (isSheetModal) { + style.setProperty('--ion-safe-area-top', '0px'); + style.setProperty('--ion-safe-area-left', '0px'); + style.setProperty('--ion-safe-area-right', '0px'); + return; + } + + // Card modals have rounded top corners + if (isCardModal) { + style.setProperty('--ion-safe-area-top', '0px'); + if (isTablet) { + // On tablets, card modals are inset from all edges + this.zeroAllSafeAreas(); + } else { + // On phones, card modals still extend to the bottom edge + style.setProperty('--ion-safe-area-left', '0px'); + style.setProperty('--ion-safe-area-right', '0px'); + this.applyFullscreenSafeArea(); + } + return; + } + + // Phone-sized fullscreen modals inherit safe areas and use wrapper padding + if (!isTablet) { + this.applyFullscreenSafeArea(); + return; + } + + // Check if tablet modal is fullscreen via CSS custom properties + const computedStyle = getComputedStyle(this.el); + const width = computedStyle.getPropertyValue('--width').trim(); + const height = computedStyle.getPropertyValue('--height').trim(); + const isFullscreen = width === '100%' && height === '100%'; + + if (isFullscreen) { + this.applyFullscreenSafeArea(); + } else { + // Centered dialog doesn't touch edges + this.zeroAllSafeAreas(); + } + } + + /** + * Applies safe-area handling for fullscreen modals. + * Adds wrapper padding when no footer is present to prevent + * content from overlapping system navigation areas. + */ + private applyFullscreenSafeArea() { + this.skipSafeAreaCoordinateDetection = true; + this.updateFooterPadding(); + + // Watch for dynamic footer additions/removals (e.g., async data loading) + // Use subtree:true to support wrapped footers in framework components + // (e.g., ...) + if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) { + this.footerObserver = new MutationObserver(() => this.updateFooterPadding()); + this.footerObserver.observe(this.el, { childList: true, subtree: true }); + } + } + + /** + * Updates wrapper padding based on footer presence. + * Called initially and when footer is dynamically added/removed. + */ + private updateFooterPadding() { + if (!this.wrapperEl) return; + + const hasFooter = this.el.querySelector('ion-footer') !== null; + if (hasFooter) { + this.wrapperEl.style.removeProperty('padding-bottom'); + this.wrapperEl.style.removeProperty('box-sizing'); + } else { + this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)'); + this.wrapperEl.style.setProperty('box-sizing', 'border-box'); + } + } + + /** + * Sets all safe-area CSS variables to 0px for modals that + * don't touch screen edges. + */ + private zeroAllSafeAreas() { + const style = this.el.style; + style.setProperty('--ion-safe-area-top', '0px'); + style.setProperty('--ion-safe-area-bottom', '0px'); + style.setProperty('--ion-safe-area-left', '0px'); + style.setProperty('--ion-safe-area-right', '0px'); + } + + /** + * Resets all safe-area related state and styles. + * Called during dismiss and disconnectedCallback to ensure clean state + * for re-presentation of inline modals. + */ + private resetSafeAreaState() { + this.skipSafeAreaCoordinateDetection = false; + this.cachedSafeAreas = undefined; + this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false }; + this.footerObserver?.disconnect(); + this.footerObserver = undefined; + + // Clear wrapper styles that may have been set for safe-area handling + if (this.wrapperEl) { + this.wrapperEl.style.removeProperty('padding-bottom'); + this.wrapperEl.style.removeProperty('box-sizing'); + } + + // Clear safe-area CSS variable overrides + const style = this.el.style; + style.removeProperty('--ion-safe-area-top'); + style.removeProperty('--ion-safe-area-bottom'); + style.removeProperty('--ion-safe-area-left'); + style.removeProperty('--ion-safe-area-right'); + } + + /** + * Gets the root safe-area values from the document element. + * Uses cached values during gestures to avoid getComputedStyle calls. + */ + private getSafeAreaValues(): { top: number; bottom: number; left: number; right: number } { + if (!this.cachedSafeAreas) { + const rootStyle = getComputedStyle(document.documentElement); + this.cachedSafeAreas = { + top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0, + bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0, + left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0, + right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0, + }; + } + return this.cachedSafeAreas; + } + + /** + * Updates safe-area CSS variable overrides based on whether the modal + * extends into each safe-area region. Called after animation + * and during gestures to handle dynamic position changes. + * + * Optimized to avoid redundant DOM writes by tracking previous state. + */ + private updateSafeAreaOverrides() { + if (this.skipSafeAreaCoordinateDetection) { + return; + } + + const wrapper = this.wrapperEl; + if (!wrapper) { + return; + } + + const rect = wrapper.getBoundingClientRect(); + const safeAreas = this.getSafeAreaValues(); + + const extendsIntoTop = rect.top < safeAreas.top; + const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom; + const extendsIntoLeft = rect.left < safeAreas.left; + const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right; + + // Only update DOM when state actually changes + const prev = this.prevSafeAreaState; + const style = this.el.style; + + if (extendsIntoTop !== prev.top) { + extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px'); + prev.top = extendsIntoTop; + } + if (extendsIntoBottom !== prev.bottom) { + extendsIntoBottom + ? style.removeProperty('--ion-safe-area-bottom') + : style.setProperty('--ion-safe-area-bottom', '0px'); + prev.bottom = extendsIntoBottom; + } + if (extendsIntoLeft !== prev.left) { + extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px'); + prev.left = extendsIntoLeft; + } + if (extendsIntoRight !== prev.right) { + extendsIntoRight + ? style.removeProperty('--ion-safe-area-right') + : style.setProperty('--ion-safe-area-right', '0px'); + prev.right = extendsIntoRight; + } + } + private sheetOnDismiss() { /** * While the gesture animation is finishing @@ -961,6 +1187,8 @@ export class Modal implements ComponentInterface, OverlayInterface { } this.currentBreakpoint = undefined; this.animation = undefined; + // Reset safe-area state for potential re-presentation + this.resetSafeAreaState(); unlock(); diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Chrome-linux.png index dcd19712e99..39ff1e2ba52 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Firefox-linux.png index aa87ce45424..7f5fa6082a4 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Chrome-linux.png index d627a28c705..7e4f8efa019 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Firefox-linux.png index 90c3297b3e6..c3a3fbb40a9 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Chrome-linux.png index 3013b111828..9c783d5e500 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Firefox-linux.png index e9ab0e35017..c4742c5a055 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Safari-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Safari-linux.png index f484160464c..a738598707e 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Safari-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Chrome-linux.png index 50a1560e71a..0a235e6efd9 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Firefox-linux.png index aa3076fd43a..bb10b08013c 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Safari-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Safari-linux.png index 15896e6b684..ac35e488c60 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Safari-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Chrome-linux.png index a91b1bf3bb4..e306148728c 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Firefox-linux.png index a2ea869f72a..15724725c20 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Safari-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Safari-linux.png index b56f30abc74..cb3f4586995 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Chrome-linux.png index bd83c2164c9..5d912a81961 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Firefox-linux.png index 1e264be58e1..0cdc03eadd8 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Safari-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Safari-linux.png index 65fab21a0c7..ef9f46f34f3 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Safari-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Chrome-linux.png index 2ac52f2d9c6..dafbde597d9 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Firefox-linux.png index cd03d981152..70371a95e14 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Safari-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Safari-linux.png index 0faf177715b..5210ca2078f 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Safari-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Chrome-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Chrome-linux.png index d2b1d607bf6..d3fb2697565 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Firefox-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Firefox-linux.png index 2b7093b0eb4..398589e8c0d 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Safari-linux.png b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Safari-linux.png index 83c959aa1f4..1f71e539b3f 100644 Binary files a/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Safari-linux.png and b/core/src/components/modal/test/basic/modal.e2e.ts-snapshots/modal-basic-present-tablet-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Chrome-linux.png index f188eb46258..44929c42bed 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Firefox-linux.png index 9c0ef9a90c4..8d61f2883cb 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Safari-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Safari-linux.png index d45c49fe7fe..4399b2afa3b 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Chrome-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Chrome-linux.png index 599fbd77e76..7393cf7a796 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Chrome-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Firefox-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Firefox-linux.png index eb7dff3ea17..446e0d64206 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Firefox-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Safari-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Safari-linux.png index a3317be484b..e37a220e0a2 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Safari-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-ios-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Chrome-linux.png index a9538fbdab2..311e61d141c 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Firefox-linux.png index 6ab9a6430e6..318a8b4bea6 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Safari-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Safari-linux.png index ac69eb9a371..5ba4557d9da 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Safari-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Chrome-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Chrome-linux.png index dca58f00ee6..8e140f3c714 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Chrome-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Firefox-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Firefox-linux.png index 79d4376a7e7..05d96cb99ba 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Firefox-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Safari-linux.png b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Safari-linux.png index 3c08445786a..0fadc9eb0f1 100644 Binary files a/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Safari-linux.png and b/core/src/components/modal/test/custom/modal.e2e.ts-snapshots/modal-custom-present-md-rtl-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Chrome-linux.png index 35395c2f9b1..d9ef05c11e8 100644 Binary files a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Firefox-linux.png index c25f3f922d1..cc754a6685d 100644 Binary files a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Safari-linux.png b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Safari-linux.png index 82482d6927e..dc426c19186 100644 Binary files a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Safari-linux.png and b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-ios-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png index 5b93a9766fc..aec170314cb 100644 Binary files a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png and b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Chrome-linux.png differ diff --git a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Firefox-linux.png b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Firefox-linux.png index 6a15fab242b..56aad283993 100644 Binary files a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Firefox-linux.png and b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Firefox-linux.png differ diff --git a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Safari-linux.png b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Safari-linux.png index b358d9fa5f9..9fa0d3587a7 100644 Binary files a/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Safari-linux.png and b/core/src/components/modal/test/dark-mode/model.e2e.ts-snapshots/modal-dark-color-md-ltr-Mobile-Safari-linux.png differ diff --git a/core/src/components/modal/test/safe-area/index.html b/core/src/components/modal/test/safe-area/index.html new file mode 100644 index 00000000000..fee7e96d079 --- /dev/null +++ b/core/src/components/modal/test/safe-area/index.html @@ -0,0 +1,263 @@ + + + + + Modal - Safe Area + + + + + + + + + + + +
+ + + Modal - Safe Area + + + + +

Test safe-area handling in modals.

+ + + + + With Footer + + + + +

Default Modal

+

Centered dialog on tablet - should NOT have safe-area padding

+
+ Present +
+ + + +

Fullscreen Modal

+

Full screen - footer handles safe-area

+
+ Present +
+ + + +

Sheet Modal (Partial)

+

At 0.5 breakpoint - should have bottom safe-area only

+
+ Present +
+ + + +

Sheet Modal (Full)

+

At 1.0 breakpoint - should have bottom safe-area

+
+ Present +
+ + + +

Card Modal (iOS)

+

Card presentation with presentingElement

+
+ Present +
+
+ + + + Without Footer (wrapper padding) + + + + +

Fullscreen Modal (no footer)

+

Wrapper padding should prevent content overlap

+
+ Present +
+ + + +

Card Modal (no footer)

+

On phones, wrapper padding should prevent content overlap

+
+ Present +
+ + + +

Default Modal (no footer)

+

On phones, wrapper padding should prevent content overlap

+
+ Present +
+
+
+
+
+
+ + + + diff --git a/core/src/components/modal/test/safe-area/modal.e2e.ts b/core/src/components/modal/test/safe-area/modal.e2e.ts new file mode 100644 index 00000000000..39bd2bb2467 --- /dev/null +++ b/core/src/components/modal/test/safe-area/modal.e2e.ts @@ -0,0 +1,252 @@ +import { expect } from '@playwright/test'; +import type { E2EPage } from '@utils/test/playwright'; +import { configs, test, Viewports } from '@utils/test/playwright'; + +/** + * Safe-area tests verify that modals correctly handle safe-area insets + * based on modal type and screen size. + * + * These tests use simulated safe-area values (34px bottom) set in index.html. + * They verify the modal wrapper has correct padding applied. + */ + +// Helper to get the modal wrapper's computed padding-bottom +async function getWrapperPaddingBottom(page: E2EPage): Promise { + const modal = page.locator('ion-modal'); + return modal.evaluate((el: HTMLIonModalElement) => { + const wrapper = el.shadowRoot?.querySelector('.modal-wrapper'); + if (!wrapper) return '0px'; + return getComputedStyle(wrapper).paddingBottom; + }); +} + +// Helper to check if modal has a footer +async function modalHasFooter(page: E2EPage): Promise { + const modal = page.locator('ion-modal'); + return modal.evaluate((el: HTMLIonModalElement) => { + return el.querySelector('ion-footer') !== null; + }); +} + +// Phone viewport (less than 768px width) +const PhoneViewport = { width: 390, height: 844 }; + +// ============================================================================= +// Phone Tests - Fullscreen modals need wrapper padding when no footer +// ============================================================================= + +configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('modal: safe-area - phone'), () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize(PhoneViewport); + await page.goto('/src/components/modal/test/safe-area', config); + }); + + test('fullscreen modal without footer should have wrapper padding', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#fullscreen-no-footer'); + await ionModalDidPresent.next(); + + const hasFooter = await modalHasFooter(page); + expect(hasFooter).toBe(false); + + const paddingBottom = await getWrapperPaddingBottom(page); + // Should have safe-area padding (34px as set in test HTML) + expect(paddingBottom).toBe('34px'); + }); + + test('fullscreen modal with footer should not have wrapper padding', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#fullscreen-modal'); + await ionModalDidPresent.next(); + + const hasFooter = await modalHasFooter(page); + expect(hasFooter).toBe(true); + + const paddingBottom = await getWrapperPaddingBottom(page); + // Footer handles safe-area, wrapper should have no padding + expect(paddingBottom).toBe('0px'); + }); + + test('default modal without footer should have wrapper padding on phone', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#default-no-footer'); + await ionModalDidPresent.next(); + + // On phones, default modals are fullscreen + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('34px'); + }); + }); +}); + +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('modal: safe-area - card modal on phone'), () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize(PhoneViewport); + await page.goto('/src/components/modal/test/safe-area', config); + }); + + test('card modal without footer should have wrapper padding on phone', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#card-modal-no-footer'); + await ionModalDidPresent.next(); + + // Card modals on phones still extend to bottom edge + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('34px'); + }); + + test('card modal with footer should not have wrapper padding', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#card-modal'); + await ionModalDidPresent.next(); + + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('0px'); + }); + }); +}); + +// ============================================================================= +// Tablet Tests - Centered dialogs don't need safe-area, fullscreen does +// ============================================================================= + +configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('modal: safe-area - tablet'), () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize(Viewports.tablet.portrait); + await page.goto('/src/components/modal/test/safe-area', config); + }); + + test('default modal should not have wrapper padding on tablet', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#default-modal'); + await ionModalDidPresent.next(); + + // Centered dialog on tablet - inset from edges, no padding needed + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('0px'); + }); + + test('fullscreen modal without footer should have wrapper padding on tablet', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#fullscreen-no-footer'); + await ionModalDidPresent.next(); + + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('34px'); + }); + + test('fullscreen modal with footer should not have wrapper padding', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#fullscreen-modal'); + await ionModalDidPresent.next(); + + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('0px'); + }); + }); +}); + +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('modal: safe-area - card modal on tablet'), () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize(Viewports.tablet.portrait); + await page.goto('/src/components/modal/test/safe-area', config); + }); + + test('card modal should not have wrapper padding on tablet', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#card-modal'); + await ionModalDidPresent.next(); + + // Card modals on tablets are inset from all edges + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('0px'); + }); + }); +}); + +// ============================================================================= +// Sheet Modal Tests - Always touch bottom edge +// ============================================================================= + +configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('modal: safe-area - sheet modal'), () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize(Viewports.tablet.portrait); + await page.goto('/src/components/modal/test/safe-area', config); + }); + + test('sheet modal should not have wrapper padding (footer handles safe-area)', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://outsystemsrd.atlassian.net/browse/FW-6830', + }); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#sheet-modal-full'); + await ionModalDidPresent.next(); + + // Sheet modals with footer - footer handles the safe area + const paddingBottom = await getWrapperPaddingBottom(page); + expect(paddingBottom).toBe('0px'); + }); + }); +}); diff --git a/core/src/components/popover/animations/ios.enter.ts b/core/src/components/popover/animations/ios.enter.ts index aa4e0568143..22e8da58d4f 100644 --- a/core/src/components/popover/animations/ios.enter.ts +++ b/core/src/components/popover/animations/ios.enter.ts @@ -61,6 +61,8 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => top, left, bottom, + checkSafeAreaTop, + checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, @@ -118,15 +120,27 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => baseEl.classList.add('popover-bottom'); } - if (bottom !== undefined) { - contentEl.style.setProperty('bottom', `${bottom}px`); - } - + /** + * Safe area CSS variable adjustments. + * When the popover is positioned near an edge, we add the corresponding + * safe-area inset to ensure the popover doesn't overlap with system UI + * (status bars, home indicators, navigation bars on Android API 36+, etc.) + */ + const safeAreaTop = ' + var(--ion-safe-area-top, 0)'; + const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)'; const safeAreaLeft = ' + var(--ion-safe-area-left, 0)'; const safeAreaRight = ' - var(--ion-safe-area-right, 0)'; + let topValue = `${top}px`; + let bottomValue = bottom !== undefined ? `${bottom}px` : undefined; let leftValue = `${left}px`; + if (checkSafeAreaTop) { + topValue = `${top}px${safeAreaTop}`; + } + if (checkSafeAreaBottom && bottomValue !== undefined) { + bottomValue = `${bottom}px${safeAreaBottom}`; + } if (checkSafeAreaLeft) { leftValue = `${left}px${safeAreaLeft}`; } @@ -134,7 +148,11 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => leftValue = `${left}px${safeAreaRight}`; } - contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`); + if (bottomValue !== undefined) { + contentEl.style.setProperty('bottom', `calc(${bottomValue})`); + } + + contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`); contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`); contentEl.style.setProperty('transform-origin', `${originY} ${originX}`); diff --git a/core/src/components/popover/animations/md.enter.ts b/core/src/components/popover/animations/md.enter.ts index e25f745cec4..e8a1e1adc5b 100644 --- a/core/src/components/popover/animations/md.enter.ts +++ b/core/src/components/popover/animations/md.enter.ts @@ -47,7 +47,17 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING; - const { originX, originY, top, left, bottom } = calculateWindowAdjustment( + const { + originX, + originY, + top, + left, + bottom, + checkSafeAreaTop, + checkSafeAreaBottom, + checkSafeAreaLeft, + checkSafeAreaRight, + } = calculateWindowAdjustment( side, results.top, results.left, @@ -62,6 +72,34 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => results.referenceCoordinates ); + /** + * Safe area CSS variable adjustments. + * When the popover is positioned near an edge, we add the corresponding + * safe-area inset to ensure the popover doesn't overlap with system UI + * (status bars, home indicators, navigation bars on Android API 36+, etc.) + */ + const safeAreaTop = ' + var(--ion-safe-area-top, 0)'; + const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)'; + const safeAreaLeft = ' + var(--ion-safe-area-left, 0)'; + const safeAreaRight = ' - var(--ion-safe-area-right, 0)'; + + let topValue = `${top}px`; + let bottomValue = bottom !== undefined ? `${bottom}px` : undefined; + let leftValue = `${left}px`; + + if (checkSafeAreaTop) { + topValue = `${top}px${safeAreaTop}`; + } + if (checkSafeAreaBottom && bottomValue !== undefined) { + bottomValue = `${bottom}px${safeAreaBottom}`; + } + if (checkSafeAreaLeft) { + leftValue = `${left}px${safeAreaLeft}`; + } + if (checkSafeAreaRight) { + leftValue = `${left}px${safeAreaRight}`; + } + const baseAnimation = createAnimation(); const backdropAnimation = createAnimation(); const wrapperAnimation = createAnimation(); @@ -81,13 +119,13 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => contentAnimation .addElement(contentEl) .beforeStyles({ - top: `calc(${top}px + var(--offset-y, 0px))`, - left: `calc(${left}px + var(--offset-x, 0px))`, + top: `calc(${topValue} + var(--offset-y, 0px))`, + left: `calc(${leftValue} + var(--offset-x, 0px))`, 'transform-origin': `${originY} ${originX}`, }) .beforeAddWrite(() => { - if (bottom !== undefined) { - contentEl.style.setProperty('bottom', `${bottom}px`); + if (bottomValue !== undefined) { + contentEl.style.setProperty('bottom', `calc(${bottomValue})`); } }) .fromTo('transform', 'scale(0.8)', 'scale(1)'); diff --git a/core/src/components/popover/test/safe-area/index.html b/core/src/components/popover/test/safe-area/index.html new file mode 100644 index 00000000000..271d5fa02c6 --- /dev/null +++ b/core/src/components/popover/test/safe-area/index.html @@ -0,0 +1,159 @@ + + + + + Popover - Safe Area + + + + + + + + + + + + +
+
+ +
+ + + Popover - Safe Area Positioning + + + + +

Test that popovers are positioned away from unsafe areas (shown in red).

+

The popover should be moved up/down to avoid overlapping the safe-area zones.

+ + + + +

Small Popover (Center)

+

Floating popover - positioned in center, no adjustment needed

+
+ Present +
+ + + +

Large Popover

+

Tall content that may extend toward bottom safe area

+
+ Present +
+
+ + Trigger Near Bottom + + Near Bottom Right + + + + + + Option 1 + Option 2 + Option 3 + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + diff --git a/core/src/components/popover/test/safe-area/popover.e2e.ts b/core/src/components/popover/test/safe-area/popover.e2e.ts new file mode 100644 index 00000000000..13658a256d0 --- /dev/null +++ b/core/src/components/popover/test/safe-area/popover.e2e.ts @@ -0,0 +1,76 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +/** + * Safe-area tests verify that popovers are correctly positioned + * to avoid overlapping with safe-area zones (status bars, navigation bars, etc.) + * + * This is especially important for Android API 36+ where edge-to-edge mode + * is enforced and apps can no longer opt out. + */ + +// Tests that apply to both iOS and MD modes +configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('popover: safe-area positioning'), () => { + test.beforeEach(async ({ page }) => { + await page.goto('/src/components/popover/test/safe-area', config); + }); + + test('popover pinned to bottom should account for safe-area-bottom in position', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30900', + }); + + // Use a smaller viewport to force the popover to be constrained + await page.setViewportSize({ width: 375, height: 500 }); + + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + + // Click the trigger near the bottom of the screen + await page.click('#bottom-trigger'); + await ionPopoverDidPresent.next(); + + // Target the specific popover that was presented (the one with trigger="bottom-trigger") + const popover = page.locator('ion-popover[trigger="bottom-trigger"]'); + const popoverContent = popover.locator('.popover-content'); + + // Get the computed bottom style - should include safe-area calc + const bottomStyle = await popoverContent.evaluate((el) => el.style.bottom); + + // The bottom should include the safe-area-bottom CSS variable + // This ensures the popover is positioned above the unsafe area + expect(bottomStyle).toContain('var(--ion-safe-area-bottom'); + }); + }); +}); + +// iOS-specific tests +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('popover: safe-area positioning - ios specific'), () => { + test.beforeEach(async ({ page }) => { + await page.goto('/src/components/popover/test/safe-area', config); + }); + + test('floating popover should not have safe-area adjustments', async ({ page }) => { + const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent'); + + await page.click('#small-popover-trigger'); + await ionPopoverDidPresent.next(); + + // Target the specific popover + const popover = page.locator('ion-popover[trigger="small-popover-trigger"]'); + const popoverContent = popover.locator('.popover-content'); + + // Get the computed top and bottom styles + const topStyle = await popoverContent.evaluate((el) => el.style.top); + const bottomStyle = await popoverContent.evaluate((el) => el.style.bottom); + + // A floating popover in the middle shouldn't have safe-area adjustments + // The top should be a simple calc without safe-area + expect(topStyle).not.toContain('var(--ion-safe-area-top'); + // The bottom should not be set for a floating popover + expect(bottomStyle).toBe(''); + }); + }); +}); diff --git a/core/src/components/popover/utils.ts b/core/src/components/popover/utils.ts index 794ebb20884..759091c01db 100644 --- a/core/src/components/popover/utils.ts +++ b/core/src/components/popover/utils.ts @@ -30,6 +30,8 @@ export interface PopoverStyles { bottom?: number; originX: string; originY: string; + checkSafeAreaTop: boolean; + checkSafeAreaBottom: boolean; checkSafeAreaLeft: boolean; checkSafeAreaRight: boolean; arrowTop: number; @@ -829,6 +831,8 @@ export const calculateWindowAdjustment = ( let bottom; let originX = contentOriginX; let originY = contentOriginY; + let checkSafeAreaTop = false; + let checkSafeAreaBottom = false; let checkSafeAreaLeft = false; let checkSafeAreaRight = false; const triggerTop = triggerCoordinates @@ -874,26 +878,57 @@ export const calculateWindowAdjustment = ( * We chose 12 here so that the popover position looks a bit nicer as * it is not right up against the edge of the screen. */ - top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1)); + top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1)); arrowTop = top + contentHeight; originY = 'bottom'; addPopoverBottomClass = true; + /** + * If the popover is positioned near the top edge, account for safe area. + * This ensures the popover doesn't overlap with status bars or notches. + */ + if (top <= bodyPadding + safeAreaMargin) { + checkSafeAreaTop = true; + top = bodyPadding; + } + /** * If not enough room for popover to appear * above trigger, then cut it off. */ } else { bottom = bodyPadding; + /** + * When the popover is pinned to the bottom, account for safe area. + * This ensures the popover doesn't overlap with home indicators + * or navigation bars (e.g., Android API 36+ edge-to-edge). + */ + checkSafeAreaBottom = true; } } + /** + * Final check: If the popover extends into any safe-area region, + * ensure the corresponding flag is set regardless of side. + * This handles cases where a side-positioned popover (left/right) + * still needs bottom safe-area padding because it extends into that region. + */ + const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight; + if (popoverBottom + safeAreaMargin > bodyHeight) { + checkSafeAreaBottom = true; + } + if (top < safeAreaMargin) { + checkSafeAreaTop = true; + } + return { top, left, bottom, originX, originY, + checkSafeAreaTop, + checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop,