@ionic/core 8.7.18-dev.11770316772.1e7b64e5 → 8.7.18-dev.11770674094.18396f54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/components/ion-refresher.js +3 -32
  2. package/components/modal.js +248 -13
  3. package/components/popover.js +152 -30
  4. package/dist/cjs/ion-modal.cjs.entry.js +248 -13
  5. package/dist/cjs/ion-popover.cjs.entry.js +152 -30
  6. package/dist/cjs/ion-refresher_2.cjs.entry.js +3 -32
  7. package/dist/collection/components/modal/modal.ios.css +0 -4
  8. package/dist/collection/components/modal/modal.js +127 -11
  9. package/dist/collection/components/modal/modal.md.css +0 -4
  10. package/dist/collection/components/modal/safe-area-utils.js +123 -0
  11. package/dist/collection/components/popover/animations/ios.enter.js +33 -8
  12. package/dist/collection/components/popover/animations/md.enter.js +26 -6
  13. package/dist/collection/components/popover/utils.js +96 -19
  14. package/dist/collection/components/refresher/refresher.js +4 -70
  15. package/dist/docs.json +4 -49
  16. package/dist/esm/ion-modal.entry.js +248 -13
  17. package/dist/esm/ion-popover.entry.js +152 -30
  18. package/dist/esm/ion-refresher_2.entry.js +3 -32
  19. package/dist/html.html-data.json +1 -1
  20. package/dist/ionic/ionic.esm.js +1 -1
  21. package/dist/ionic/p-36d78f36.entry.js +4 -0
  22. package/dist/ionic/p-639dd543.entry.js +4 -0
  23. package/dist/ionic/p-cc3446d5.entry.js +4 -0
  24. package/dist/types/components/modal/modal.d.ts +23 -0
  25. package/dist/types/components/modal/safe-area-utils.d.ts +55 -0
  26. package/dist/types/components/popover/utils.d.ts +23 -1
  27. package/dist/types/components/refresher/refresher-interface.d.ts +0 -7
  28. package/dist/types/components/refresher/refresher.d.ts +3 -16
  29. package/dist/types/components.d.ts +5 -16
  30. package/dist/types/interface.d.ts +1 -1
  31. package/hydrate/index.js +403 -75
  32. package/hydrate/index.mjs +403 -75
  33. package/package.json +1 -1
  34. package/dist/ionic/p-90f2d3ff.entry.js +0 -4
  35. package/dist/ionic/p-91840a80.entry.js +0 -4
  36. package/dist/ionic/p-f9061316.entry.js +0 -4
package/hydrate/index.js CHANGED
@@ -22806,9 +22806,129 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22806
22806
  };
22807
22807
  };
22808
22808
 
22809
- const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}";
22809
+ /**
22810
+ * These thresholds match the SCSS media query breakpoints in modal.vars.scss
22811
+ * that trigger the centered dialog layout (non-fullscreen modal).
22812
+ *
22813
+ * SCSS defines two height breakpoints: $modal-inset-min-height-small (600px)
22814
+ * and $modal-inset-min-height-large (768px). We use the smaller one because
22815
+ * that's the threshold where the modal transitions from fullscreen to centered
22816
+ * dialog — the larger breakpoint only increases the dialog's height.
22817
+ */
22818
+ const MODAL_INSET_MIN_WIDTH = 768;
22819
+ const MODAL_INSET_MIN_HEIGHT = 600;
22820
+ const EDGE_THRESHOLD = 5;
22821
+ /**
22822
+ * Determines if the current viewport meets the CSS media query conditions
22823
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
22824
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
22825
+ */
22826
+ const isCenteredDialogViewport = () => {
22827
+ if (!win$1)
22828
+ return false;
22829
+ return win$1.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
22830
+ .matches;
22831
+ };
22832
+ /**
22833
+ * Returns the initial safe-area configuration based on modal type.
22834
+ * This is called before animation starts and uses configuration-based prediction.
22835
+ *
22836
+ * @param context - Modal context information
22837
+ * @returns SafeAreaConfig with initial safe-area values
22838
+ */
22839
+ const getInitialSafeAreaConfig = (context) => {
22840
+ const { isSheetModal, isCardModal } = context;
22841
+ // Sheet modals use bottom safe-area, and top safe-area only when fully expanded
22842
+ if (isSheetModal) {
22843
+ return {
22844
+ top: context.currentBreakpoint === 1 ? 'inherit' : '0px',
22845
+ bottom: 'inherit',
22846
+ left: '0px',
22847
+ right: '0px',
22848
+ };
22849
+ }
22850
+ // Card modals need safe-area for height calculation.
22851
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
22852
+ if (isCardModal) {
22853
+ return {
22854
+ top: 'inherit',
22855
+ bottom: 'inherit',
22856
+ left: '0px',
22857
+ right: '0px',
22858
+ };
22859
+ }
22860
+ // On viewports that meet the centered dialog media query breakpoints,
22861
+ // regular modals render as centered dialogs (not fullscreen), so they
22862
+ // don't touch any screen edges and don't need safe-area insets.
22863
+ if (isCenteredDialogViewport()) {
22864
+ return {
22865
+ top: '0px',
22866
+ bottom: '0px',
22867
+ left: '0px',
22868
+ right: '0px',
22869
+ };
22870
+ }
22871
+ // Fullscreen modals on phone - inherit all safe areas
22872
+ return {
22873
+ top: 'inherit',
22874
+ bottom: 'inherit',
22875
+ left: 'inherit',
22876
+ right: 'inherit',
22877
+ };
22878
+ };
22879
+ /**
22880
+ * Returns safe-area configuration based on actual modal position.
22881
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
22882
+ *
22883
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
22884
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
22885
+ * prediction instead. Regular modals use coordinate detection which works reliably
22886
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
22887
+ * once a reliable detection mechanism is available.
22888
+ *
22889
+ * @param wrapperEl - The modal wrapper element to measure
22890
+ * @returns SafeAreaConfig based on position
22891
+ */
22892
+ const getPositionBasedSafeAreaConfig = (wrapperEl) => {
22893
+ var _a, _b;
22894
+ const rect = wrapperEl.getBoundingClientRect();
22895
+ const vh = (_a = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerHeight) !== null && _a !== void 0 ? _a : 0;
22896
+ const vw = (_b = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerWidth) !== null && _b !== void 0 ? _b : 0;
22897
+ // Only apply safe-area to sides where modal overlaps with screen edge
22898
+ return {
22899
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
22900
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
22901
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
22902
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
22903
+ };
22904
+ };
22905
+ /**
22906
+ * Applies safe-area CSS custom property overrides to the modal host element.
22907
+ *
22908
+ * @param hostEl - The modal host element (ion-modal)
22909
+ * @param config - Safe-area configuration to apply
22910
+ */
22911
+ const applySafeAreaOverrides = (hostEl, config) => {
22912
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
22913
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
22914
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
22915
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
22916
+ };
22917
+ /**
22918
+ * Clears safe-area CSS custom property overrides from the modal host element.
22919
+ *
22920
+ * @param hostEl - The modal host element (ion-modal)
22921
+ */
22922
+ const clearSafeAreaOverrides = (hostEl) => {
22923
+ hostEl.style.removeProperty('--ion-safe-area-top');
22924
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
22925
+ hostEl.style.removeProperty('--ion-safe-area-left');
22926
+ hostEl.style.removeProperty('--ion-safe-area-right');
22927
+ };
22928
+
22929
+ const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}";
22810
22930
 
22811
- const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0, 40px, 0);transform:translate3d(0, 40px, 0);opacity:0.01}";
22931
+ const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0, 40px, 0);transform:translate3d(0, 40px, 0);opacity:0.01}";
22812
22932
 
22813
22933
  // TODO(FW-2832): types
22814
22934
  /**
@@ -23054,6 +23174,9 @@ class Modal {
23054
23174
  this.triggerController.removeClickListener();
23055
23175
  this.cleanupViewTransitionListener();
23056
23176
  this.cleanupParentRemovalObserver();
23177
+ // Also called in dismiss() — intentional dual cleanup covers both
23178
+ // dismiss-then-remove and direct DOM removal without dismiss.
23179
+ this.cleanupSafeAreaOverrides();
23057
23180
  }
23058
23181
  componentWillLoad() {
23059
23182
  var _a;
@@ -23214,6 +23337,11 @@ class Modal {
23214
23337
  await waitForMount();
23215
23338
  }
23216
23339
  writeTask(() => this.el.classList.add('show-modal'));
23340
+ // Recalculate isSheetModal before safe-area setup because framework
23341
+ // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
23342
+ this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23343
+ // Set initial safe-area overrides before animation
23344
+ this.setInitialSafeAreaOverrides();
23217
23345
  const hasCardModal = presentingElement !== undefined;
23218
23346
  /**
23219
23347
  * We need to change the status bar at the
@@ -23231,6 +23359,10 @@ class Modal {
23231
23359
  backdropBreakpoint: this.backdropBreakpoint,
23232
23360
  expandToScroll: this.expandToScroll,
23233
23361
  });
23362
+ // Update safe-area based on actual position after animation
23363
+ this.updateSafeAreaOverrides();
23364
+ // Apply fullscreen safe-area padding if needed
23365
+ this.applyFullscreenSafeArea();
23234
23366
  /* tslint:disable-next-line */
23235
23367
  if (typeof window !== 'undefined') {
23236
23368
  /**
@@ -23262,13 +23394,7 @@ class Modal {
23262
23394
  };
23263
23395
  window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
23264
23396
  }
23265
- /**
23266
- * Recalculate isSheetModal because framework bindings (e.g., Angular)
23267
- * may not have been applied when componentWillLoad ran.
23268
- */
23269
- const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23270
- this.isSheetModal = isSheetModal;
23271
- if (isSheetModal) {
23397
+ if (this.isSheetModal) {
23272
23398
  this.initSheetGesture();
23273
23399
  }
23274
23400
  else if (hasCardModal) {
@@ -23345,6 +23471,8 @@ class Modal {
23345
23471
  if (this.currentBreakpoint !== breakpoint) {
23346
23472
  this.currentBreakpoint = breakpoint;
23347
23473
  this.ionBreakpointDidChange.emit({ breakpoint });
23474
+ // Update safe-area overrides based on new position
23475
+ this.updateSafeAreaOverrides();
23348
23476
  }
23349
23477
  });
23350
23478
  this.gesture = gesture;
@@ -23512,6 +23640,7 @@ class Modal {
23512
23640
  }
23513
23641
  this.cleanupViewTransitionListener();
23514
23642
  this.cleanupParentRemovalObserver();
23643
+ this.cleanupSafeAreaOverrides();
23515
23644
  this.cleanupChildRoutePassthrough();
23516
23645
  }
23517
23646
  this.currentBreakpoint = undefined;
@@ -23632,6 +23761,9 @@ class Modal {
23632
23761
  this.viewTransitionAnimation = transitionAnimation;
23633
23762
  transitionAnimation.play().then(() => {
23634
23763
  this.viewTransitionAnimation = undefined;
23764
+ // Wait for a layout pass after the transition so getBoundingClientRect()
23765
+ // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
23766
+ raf(() => this.updateSafeAreaOverrides());
23635
23767
  // After orientation transition, recreate the swipe-to-close gesture
23636
23768
  // with updated animation that reflects the new presenting element state
23637
23769
  this.reinitSwipeToClose();
@@ -23758,6 +23890,109 @@ class Modal {
23758
23890
  (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
23759
23891
  this.parentRemovalObserver = undefined;
23760
23892
  }
23893
+ /**
23894
+ * Creates the context object for safe-area utilities.
23895
+ */
23896
+ getSafeAreaContext() {
23897
+ return {
23898
+ isSheetModal: this.isSheetModal,
23899
+ isCardModal: this.presentingElement !== undefined && getIonMode$1(this) === 'ios',
23900
+ presentingElement: this.presentingElement,
23901
+ breakpoints: this.breakpoints,
23902
+ currentBreakpoint: this.currentBreakpoint,
23903
+ };
23904
+ }
23905
+ /**
23906
+ * Sets initial safe-area overrides before modal animation.
23907
+ * Called in present() before animation starts.
23908
+ */
23909
+ setInitialSafeAreaOverrides() {
23910
+ const context = this.getSafeAreaContext();
23911
+ const safeAreaConfig = getInitialSafeAreaConfig(context);
23912
+ applySafeAreaOverrides(this.el, safeAreaConfig);
23913
+ }
23914
+ /**
23915
+ * Updates safe-area overrides during dynamic state changes.
23916
+ * Called after animations, during gestures, and on orientation changes.
23917
+ */
23918
+ updateSafeAreaOverrides() {
23919
+ const { wrapperEl, el } = this;
23920
+ const context = this.getSafeAreaContext();
23921
+ // Sheet modals: the wrapper extends beyond the viewport and is translated
23922
+ // via breakpoint gestures, making getBoundingClientRect unreliable for
23923
+ // edge detection. Instead, use breakpoint value to determine top safe-area.
23924
+ if (context.isSheetModal) {
23925
+ const needsTopSafeArea = context.currentBreakpoint === 1;
23926
+ applySafeAreaOverrides(el, {
23927
+ top: needsTopSafeArea ? 'inherit' : '0px',
23928
+ bottom: 'inherit',
23929
+ left: '0px',
23930
+ right: '0px',
23931
+ });
23932
+ return;
23933
+ }
23934
+ // Card modals have fixed safe-area requirements set by initial prediction.
23935
+ if (context.isCardModal)
23936
+ return;
23937
+ // wrapperEl is required for position-based detection below
23938
+ if (!wrapperEl)
23939
+ return;
23940
+ // Regular modals: use position-based detection to correctly handle both
23941
+ // fullscreen modals and centered dialogs with custom dimensions.
23942
+ const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
23943
+ applySafeAreaOverrides(el, safeAreaConfig);
23944
+ }
23945
+ /**
23946
+ * Applies padding-bottom to fullscreen modal wrapper to prevent
23947
+ * content from overlapping system navigation bar.
23948
+ */
23949
+ applyFullscreenSafeArea() {
23950
+ const { wrapperEl, el } = this;
23951
+ if (!wrapperEl)
23952
+ return;
23953
+ const context = this.getSafeAreaContext();
23954
+ if (context.isSheetModal || context.isCardModal)
23955
+ return;
23956
+ // Check for standard Ionic layout children (ion-content, ion-footer),
23957
+ // searching one level deep for wrapped components (e.g.,
23958
+ // <app-footer><ion-footer>...</ion-footer></app-footer>).
23959
+ let hasContent = false;
23960
+ let hasFooter = false;
23961
+ for (const child of Array.from(el.children)) {
23962
+ if (child.tagName === 'ION-CONTENT')
23963
+ hasContent = true;
23964
+ if (child.tagName === 'ION-FOOTER')
23965
+ hasFooter = true;
23966
+ for (const grandchild of Array.from(child.children)) {
23967
+ if (grandchild.tagName === 'ION-CONTENT')
23968
+ hasContent = true;
23969
+ if (grandchild.tagName === 'ION-FOOTER')
23970
+ hasFooter = true;
23971
+ }
23972
+ }
23973
+ // Only apply wrapper padding for standard Ionic layouts (has ion-content
23974
+ // but no ion-footer). Custom modals with raw HTML are fully
23975
+ // developer-controlled and should not be modified.
23976
+ if (!hasContent || hasFooter)
23977
+ return;
23978
+ // Reduce wrapper height by safe-area and add equivalent padding so the
23979
+ // total visual size stays the same but the flex content area shrinks.
23980
+ // Using height + padding instead of box-sizing: border-box avoids
23981
+ // breaking custom modals that set --border-width (border-box would
23982
+ // include the border inside the height, changing the layout).
23983
+ wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
23984
+ wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23985
+ }
23986
+ /**
23987
+ * Clears all safe-area overrides and padding from wrapper.
23988
+ */
23989
+ cleanupSafeAreaOverrides() {
23990
+ clearSafeAreaOverrides(this.el);
23991
+ if (this.wrapperEl) {
23992
+ this.wrapperEl.style.removeProperty('height');
23993
+ this.wrapperEl.style.removeProperty('padding-bottom');
23994
+ }
23995
+ }
23761
23996
  render() {
23762
23997
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
23763
23998
  const showHandle = handle !== false && isSheetModal;
@@ -23765,20 +24000,20 @@ class Modal {
23765
24000
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23766
24001
  const isHandleCycle = handleBehavior === 'cycle';
23767
24002
  const isSheetModalWithHandle = isSheetModal && showHandle;
23768
- return (hAsync(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
24003
+ return (hAsync(Host, Object.assign({ key: 'f6c1245b5458693f19a040b362a98140c571297f', "no-router": true,
23769
24004
  // Allow the modal to be navigable when the handle is focusable
23770
24005
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23771
24006
  zIndex: `${20000 + this.overlayIndex}`,
23772
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), hAsync("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), hAsync("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
24007
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), hAsync("ion-backdrop", { key: '6866fb2a265d40c761ed6bfb661a217948c571ef', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '00788de9267f1c064f16aa55f35250de16522ae6', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'a575e1755bd25a7996cdb04b014a91d538e7bc94',
23773
24008
  /*
23774
24009
  role and aria-modal must be used on the
23775
24010
  same element. They must also be set inside the
23776
24011
  shadow DOM otherwise ion-button will not be highlighted
23777
24012
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23778
24013
  */
23779
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
24014
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: 'b99c64717037e9241479ea2f5bb84e79a466d918', class: "modal-handle",
23780
24015
  // Prevents the handle from receiving keyboard focus when it does not cycle
23781
- tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), hAsync("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
24016
+ tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), hAsync("slot", { key: 'ee672afd537b281aeeb73a55d61ceeb5dfae212a', onSlotchange: this.onSlotChange }))));
23782
24017
  }
23783
24018
  get el() { return getElement(this); }
23784
24019
  static get watchers() { return {
@@ -26712,6 +26947,58 @@ class PickerColumnOption {
26712
26947
  }; }
26713
26948
  }
26714
26949
 
26950
+ /**
26951
+ * Shared per-frame cache for safe-area insets. Avoids creating a temporary
26952
+ * DOM element and forcing a synchronous reflow on every call within the same
26953
+ * frame (e.g., multiple popovers presenting simultaneously). Invalidated
26954
+ * after the next animation frame so values stay fresh across orientation
26955
+ * changes and viewport resizes.
26956
+ */
26957
+ let cachedInsets = null;
26958
+ let cacheInvalidationScheduled = false;
26959
+ /**
26960
+ * Resolves the current --ion-safe-area-* CSS custom property values
26961
+ * to actual pixel numbers. A temporary element is needed because
26962
+ * getComputedStyle on :root returns the declared value of custom
26963
+ * properties (e.g. "env(safe-area-inset-top)") rather than a
26964
+ * resolved number. By assigning them to standard padding properties,
26965
+ * the browser resolves the value.
26966
+ *
26967
+ * Results are cached for the current frame to avoid repeated reflows.
26968
+ */
26969
+ const getSafeAreaInsets = (doc) => {
26970
+ if (cachedInsets !== null) {
26971
+ return cachedInsets;
26972
+ }
26973
+ if (doc.body === null) {
26974
+ return { top: 0, bottom: 0, left: 0, right: 0 };
26975
+ }
26976
+ const el = doc.createElement('div');
26977
+ el.style.cssText =
26978
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
26979
+ 'padding-top:var(--ion-safe-area-top,0px);' +
26980
+ 'padding-bottom:var(--ion-safe-area-bottom,0px);' +
26981
+ 'padding-left:var(--ion-safe-area-left,0px);' +
26982
+ 'padding-right:var(--ion-safe-area-right,0px);';
26983
+ doc.body.appendChild(el);
26984
+ const style = getComputedStyle(el);
26985
+ const insets = {
26986
+ top: parseFloat(style.paddingTop) || 0,
26987
+ bottom: parseFloat(style.paddingBottom) || 0,
26988
+ left: parseFloat(style.paddingLeft) || 0,
26989
+ right: parseFloat(style.paddingRight) || 0,
26990
+ };
26991
+ el.remove();
26992
+ cachedInsets = insets;
26993
+ if (!cacheInvalidationScheduled) {
26994
+ cacheInvalidationScheduled = true;
26995
+ raf(() => {
26996
+ cachedInsets = null;
26997
+ cacheInvalidationScheduled = false;
26998
+ });
26999
+ }
27000
+ return insets;
27001
+ };
26715
27002
  /**
26716
27003
  * Returns the dimensions of the popover
26717
27004
  * arrow on `ios` mode. If arrow is disabled
@@ -27347,8 +27634,10 @@ const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, con
27347
27634
  * Adjusts popover positioning coordinates
27348
27635
  * such that popover does not appear offscreen
27349
27636
  * or overlapping safe area bounds.
27637
+ *
27638
+ * @internal - This is not part of the public API.
27350
27639
  */
27351
- const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeAreaMargin, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
27640
+ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
27352
27641
  let arrowTop = coordArrowTop;
27353
27642
  const arrowLeft = coordArrowLeft;
27354
27643
  let left = coordLeft;
@@ -27358,16 +27647,19 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27358
27647
  let originY = contentOriginY;
27359
27648
  let checkSafeAreaLeft = false;
27360
27649
  let checkSafeAreaRight = false;
27650
+ let checkSafeAreaTop = false;
27651
+ let checkSafeAreaBottom = false;
27361
27652
  const triggerTop = triggerCoordinates
27362
27653
  ? triggerCoordinates.top + triggerCoordinates.height
27363
27654
  : bodyHeight / 2 - contentHeight / 2;
27364
27655
  const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
27365
27656
  let addPopoverBottomClass = false;
27657
+ const hideArrow = false;
27366
27658
  /**
27367
27659
  * Adjust popover so it does not
27368
27660
  * go off the left of the screen.
27369
27661
  */
27370
- if (left < bodyPadding + safeAreaMargin) {
27662
+ if (left < bodyPadding + safeArea.left) {
27371
27663
  left = bodyPadding;
27372
27664
  checkSafeAreaLeft = true;
27373
27665
  originX = 'left';
@@ -27376,41 +27668,58 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27376
27668
  * go off the right of the screen.
27377
27669
  */
27378
27670
  }
27379
- else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) {
27671
+ else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
27380
27672
  checkSafeAreaRight = true;
27381
27673
  left = bodyWidth - contentWidth - bodyPadding;
27382
27674
  originX = 'right';
27383
27675
  }
27384
27676
  /**
27385
27677
  * Adjust popover so it does not
27386
- * go off the top of the screen.
27678
+ * go off the bottom of the screen
27679
+ * or overlap the bottom safe area.
27387
27680
  * If popover is on the left or the right of
27388
27681
  * the trigger, then we should not adjust top
27389
27682
  * margins.
27390
27683
  */
27391
- if (triggerTop + triggerHeight + contentHeight > bodyHeight && (side === 'top' || side === 'bottom')) {
27392
- if (triggerTop - contentHeight > 0) {
27684
+ if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
27685
+ (side === 'top' || side === 'bottom')) {
27686
+ /**
27687
+ * Calculate where the popover top would be if flipped
27688
+ * above the trigger. Check whether that position clears
27689
+ * the top safe area with room for bodyPadding.
27690
+ */
27691
+ const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
27692
+ if (idealFlipTop >= safeArea.top + bodyPadding) {
27393
27693
  /**
27394
- * While we strive to align the popover with the trigger
27395
- * on smaller screens this is not always possible. As a result,
27396
- * we adjust the popover up so that it does not hang
27397
- * off the bottom of the screen. However, we do not want to move
27398
- * the popover up so much that it goes off the top of the screen.
27399
- *
27400
- * We chose 12 here so that the popover position looks a bit nicer as
27401
- * it is not right up against the edge of the screen.
27694
+ * Popover fits above the trigger without overlapping
27695
+ * the top safe area. Use the ideal position directly —
27696
+ * no safe-area CSS vars needed since it clears both edges.
27402
27697
  */
27403
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27698
+ top = idealFlipTop;
27404
27699
  arrowTop = top + contentHeight;
27405
27700
  originY = 'bottom';
27406
27701
  addPopoverBottomClass = true;
27407
- /**
27408
- * If not enough room for popover to appear
27409
- * above trigger, then cut it off.
27410
- */
27411
27702
  }
27412
27703
  else {
27704
+ /**
27705
+ * Can't flip above the trigger. Constrain the bottom
27706
+ * edge while keeping the top near the trigger. This
27707
+ * creates a scrollable area anchored to the trigger.
27708
+ */
27413
27709
  bottom = bodyPadding;
27710
+ checkSafeAreaBottom = true;
27711
+ /**
27712
+ * When the trigger is near the bottom of the screen,
27713
+ * the calculated top may be at or past the bottom
27714
+ * constraint, leaving zero visible height. In that
27715
+ * case, pull top up to the top safe area so the
27716
+ * popover fills the available space between safe areas.
27717
+ */
27718
+ const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
27719
+ if (top >= bottomEdge) {
27720
+ top = safeArea.top + bodyPadding;
27721
+ checkSafeAreaTop = true;
27722
+ }
27414
27723
  }
27415
27724
  }
27416
27725
  return {
@@ -27421,9 +27730,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27421
27730
  originY,
27422
27731
  checkSafeAreaLeft,
27423
27732
  checkSafeAreaRight,
27733
+ checkSafeAreaTop,
27734
+ checkSafeAreaBottom,
27424
27735
  arrowTop,
27425
27736
  arrowLeft,
27426
27737
  addPopoverBottomClass,
27738
+ hideArrow,
27427
27739
  };
27428
27740
  };
27429
27741
  const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
@@ -27453,6 +27765,15 @@ const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
27453
27765
  };
27454
27766
 
27455
27767
  const POPOVER_IOS_BODY_PADDING = 5;
27768
+ /**
27769
+ * Minimum edge margin for iOS popovers ensures visual spacing from screen
27770
+ * edges on devices without safe areas (e.g., older iPhones without notches).
27771
+ * Previously this was a hardcoded `safeAreaMargin = 25` that served dual
27772
+ * purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
27773
+ * insets are read dynamically, this floor preserves the visual spacing when
27774
+ * safe-area values are 0.
27775
+ */
27776
+ const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
27456
27777
  /**
27457
27778
  * iOS Popover Enter Animation
27458
27779
  */
@@ -27478,8 +27799,16 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27478
27799
  };
27479
27800
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27480
27801
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
27481
- const margin = size === 'cover' ? 0 : 25;
27482
- const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
27802
+ const rawSafeArea = getSafeAreaInsets(doc);
27803
+ const safeArea = size === 'cover'
27804
+ ? { top: 0, bottom: 0, left: 0, right: 0 }
27805
+ : {
27806
+ top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
27807
+ bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
27808
+ left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
27809
+ right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
27810
+ };
27811
+ const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, arrowTop, arrowLeft, addPopoverBottomClass} = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
27483
27812
  const baseAnimation = createAnimation();
27484
27813
  const backdropAnimation = createAnimation();
27485
27814
  const contentAnimation = createAnimation();
@@ -27510,10 +27839,14 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27510
27839
  baseEl.classList.add('popover-bottom');
27511
27840
  }
27512
27841
  if (bottom !== undefined) {
27513
- contentEl.style.setProperty('bottom', `${bottom}px`);
27842
+ let bottomValue = `${bottom}px`;
27843
+ if (checkSafeAreaBottom) {
27844
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
27845
+ }
27846
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27514
27847
  }
27515
- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27516
- const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27848
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
27849
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
27517
27850
  let leftValue = `${left}px`;
27518
27851
  if (checkSafeAreaLeft) {
27519
27852
  leftValue = `${left}px${safeAreaLeft}`;
@@ -27521,7 +27854,11 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27521
27854
  if (checkSafeAreaRight) {
27522
27855
  leftValue = `${left}px${safeAreaRight}`;
27523
27856
  }
27524
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27857
+ let topValue = `${top}px`;
27858
+ if (checkSafeAreaTop) {
27859
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
27860
+ }
27861
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27525
27862
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27526
27863
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27527
27864
  if (arrowEl !== null) {
@@ -27597,7 +27934,23 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27597
27934
  };
27598
27935
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27599
27936
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
27600
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
27937
+ // MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
27938
+ // This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
27939
+ const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
27940
+ const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates);
27941
+ const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
27942
+ const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
27943
+ let leftValue = `${left}px`;
27944
+ if (checkSafeAreaLeft) {
27945
+ leftValue = `${left}px${safeAreaLeftCalc}`;
27946
+ }
27947
+ if (checkSafeAreaRight) {
27948
+ leftValue = `${left}px${safeAreaRightCalc}`;
27949
+ }
27950
+ let topValue = `${top}px`;
27951
+ if (checkSafeAreaTop) {
27952
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
27953
+ }
27601
27954
  const baseAnimation = createAnimation();
27602
27955
  const backdropAnimation = createAnimation();
27603
27956
  const wrapperAnimation = createAnimation();
@@ -27614,13 +27967,17 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27614
27967
  contentAnimation
27615
27968
  .addElement(contentEl)
27616
27969
  .beforeStyles({
27617
- top: `calc(${top}px + var(--offset-y, 0px))`,
27618
- left: `calc(${left}px + var(--offset-x, 0px))`,
27970
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
27971
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
27619
27972
  'transform-origin': `${originY} ${originX}`,
27620
27973
  })
27621
27974
  .beforeAddWrite(() => {
27622
27975
  if (bottom !== undefined) {
27623
- contentEl.style.setProperty('bottom', `${bottom}px`);
27976
+ let bottomValue = `${bottom}px`;
27977
+ if (checkSafeAreaBottom) {
27978
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
27979
+ }
27980
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27624
27981
  }
27625
27982
  })
27626
27983
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -27632,7 +27989,7 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27632
27989
  if (size === 'cover') {
27633
27990
  baseEl.style.setProperty('--width', `${contentWidth}px`);
27634
27991
  }
27635
- if (originY === 'bottom') {
27992
+ if (addPopoverBottomClass) {
27636
27993
  baseEl.classList.add('popover-bottom');
27637
27994
  }
27638
27995
  })
@@ -29737,8 +30094,6 @@ class Refresher {
29737
30094
  this.ionRefresh = createEvent(this, "ionRefresh", 7);
29738
30095
  this.ionPull = createEvent(this, "ionPull", 7);
29739
30096
  this.ionStart = createEvent(this, "ionStart", 7);
29740
- this.ionPullStart = createEvent(this, "ionPullStart", 7);
29741
- this.ionPullEnd = createEvent(this, "ionPullEnd", 7);
29742
30097
  this.appliedStyles = false;
29743
30098
  this.didStart = false;
29744
30099
  this.progress = 0;
@@ -29793,8 +30148,8 @@ class Refresher {
29793
30148
  * than `1`. The default value is `1` which is equal to the speed of the cursor.
29794
30149
  * If a negative value is passed in, the factor will be `1` instead.
29795
30150
  *
29796
- * For example, If the value passed is `1.2` and the content is dragged by
29797
- * `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels
30151
+ * For example: If the value passed is `1.2` and the content is dragged by
30152
+ * `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
29798
30153
  * (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
29799
30154
  * will be `8` pixels, less than the amount the cursor has moved.
29800
30155
  *
@@ -29844,9 +30199,6 @@ class Refresher {
29844
30199
  this.animations = [];
29845
30200
  this.progress = 0;
29846
30201
  this.state = 1 /* RefresherState.Inactive */;
29847
- this.ionPullEnd.emit({
29848
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
29849
- });
29850
30202
  }
29851
30203
  async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
29852
30204
  this.elementToTransform = this.scrollEl;
@@ -29879,7 +30231,6 @@ class Refresher {
29879
30231
  if (!this.didStart) {
29880
30232
  this.didStart = true;
29881
30233
  this.ionStart.emit();
29882
- this.ionPullStart.emit();
29883
30234
  }
29884
30235
  // emit "pulling" on every move
29885
30236
  if (this.pointerDown) {
@@ -29955,7 +30306,6 @@ class Refresher {
29955
30306
  this.lastVelocityY = ev.velocityY;
29956
30307
  },
29957
30308
  onEnd: () => {
29958
- const hadStarted = this.didStart;
29959
30309
  this.pointerDown = false;
29960
30310
  this.didStart = false;
29961
30311
  if (this.needsCompletion) {
@@ -29965,13 +30315,6 @@ class Refresher {
29965
30315
  else if (this.didRefresh) {
29966
30316
  readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
29967
30317
  }
29968
- else if (hadStarted) {
29969
- /**
29970
- * User started pulling but released before reaching the refresh threshold.
29971
- * Emit ionPullEnd to complete the event pair.
29972
- */
29973
- this.ionPullEnd.emit({ reason: 'cancel' });
29974
- }
29975
30318
  },
29976
30319
  });
29977
30320
  this.disabledChanged();
@@ -30018,7 +30361,6 @@ class Refresher {
30018
30361
  ev.data.animation = animation;
30019
30362
  animation.progressStart(false, 0);
30020
30363
  this.ionStart.emit();
30021
- this.ionPullStart.emit();
30022
30364
  this.animations.push(animation);
30023
30365
  return;
30024
30366
  }
@@ -30041,7 +30383,6 @@ class Refresher {
30041
30383
  this.animations = [];
30042
30384
  this.gesture.enable(true);
30043
30385
  this.state = 1 /* RefresherState.Inactive */;
30044
- this.ionPullEnd.emit({ reason: 'cancel' });
30045
30386
  });
30046
30387
  return;
30047
30388
  }
@@ -30286,7 +30627,6 @@ class Refresher {
30286
30627
  if (!this.didStart) {
30287
30628
  this.didStart = true;
30288
30629
  this.ionStart.emit();
30289
- this.ionPullStart.emit();
30290
30630
  }
30291
30631
  // emit "pulling" on every move
30292
30632
  this.ionPull.emit();
@@ -30330,15 +30670,6 @@ class Refresher {
30330
30670
  * available right away.
30331
30671
  */
30332
30672
  this.restoreOverflowStyle();
30333
- /**
30334
- * If ionPullStart was emitted, we need to emit ionPullEnd
30335
- * even though the gesture was aborted before reaching the
30336
- * pulling threshold.
30337
- */
30338
- if (this.didStart) {
30339
- this.didStart = false;
30340
- this.ionPullEnd.emit({ reason: 'cancel' });
30341
- }
30342
30673
  }
30343
30674
  }
30344
30675
  beginRefresh() {
@@ -30385,9 +30716,6 @@ class Refresher {
30385
30716
  if (this.contentFullscreen && this.backgroundContentEl) {
30386
30717
  (_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
30387
30718
  }
30388
- this.ionPullEnd.emit({
30389
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
30390
- });
30391
30719
  }, 600);
30392
30720
  // reset the styles on the scroll element
30393
30721
  // set that the refresh is actively cancelling/completing
@@ -30441,7 +30769,7 @@ class Refresher {
30441
30769
  }
30442
30770
  render() {
30443
30771
  const mode = getIonMode$1(this);
30444
- return (hAsync(Host, { key: 'bed0c2f44c32ecac24b7d8326ac7c7ef1d12c44f', slot: "fixed", class: {
30772
+ return (hAsync(Host, { key: '2d1bd880877b698604542ab2d602d38b9504d975', slot: "fixed", class: {
30445
30773
  [mode]: true,
30446
30774
  // Used internally for styling
30447
30775
  [`refresher-${mode}`]: true,