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