@ionic/core 8.7.18-dev.11770318664.15f47a5e → 8.7.18-dev.11770916502.174efd7f

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 +326 -17
  3. package/components/popover.js +152 -30
  4. package/dist/cjs/ion-modal.cjs.entry.js +326 -17
  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 +6 -5
  8. package/dist/collection/components/modal/modal.js +164 -15
  9. package/dist/collection/components/modal/modal.md.css +6 -5
  10. package/dist/collection/components/modal/safe-area-utils.js +165 -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 +326 -17
  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-3e69999d.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 +35 -0
  25. package/dist/types/components/modal/safe-area-utils.d.ts +66 -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 +481 -79
  32. package/hydrate/index.mjs +481 -79
  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,170 @@ 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
+ * Cache for resolved root safe-area-top value, invalidated once per frame.
22823
+ */
22824
+ let cachedRootSafeAreaTop = null;
22825
+ let cacheInvalidationScheduled$1 = false;
22826
+ /**
22827
+ * Determines if the current viewport meets the CSS media query conditions
22828
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
22829
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
22830
+ */
22831
+ const isCenteredDialogViewport = () => {
22832
+ if (!win$1)
22833
+ return false;
22834
+ return win$1.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
22835
+ .matches;
22836
+ };
22837
+ /**
22838
+ * Resolves the current root --ion-safe-area-top value to pixels.
22839
+ * Uses a temporary element because getComputedStyle on :root returns
22840
+ * the declared value of custom properties (e.g. "env(safe-area-inset-top)")
22841
+ * rather than a resolved number.
22842
+ *
22843
+ * Results are cached for the current frame to avoid repeated reflows.
22844
+ */
22845
+ const getRootSafeAreaTop = () => {
22846
+ if (cachedRootSafeAreaTop !== null) {
22847
+ return cachedRootSafeAreaTop;
22848
+ }
22849
+ const doc = win$1 === null || win$1 === void 0 ? void 0 : win$1.document;
22850
+ if (!(doc === null || doc === void 0 ? void 0 : doc.body)) {
22851
+ return 0;
22852
+ }
22853
+ const el = doc.createElement('div');
22854
+ el.style.cssText =
22855
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' + 'padding-top:var(--ion-safe-area-top,0px);';
22856
+ doc.body.appendChild(el);
22857
+ const value = parseFloat(getComputedStyle(el).paddingTop) || 0;
22858
+ el.remove();
22859
+ cachedRootSafeAreaTop = value;
22860
+ if (!cacheInvalidationScheduled$1) {
22861
+ cacheInvalidationScheduled$1 = true;
22862
+ raf(() => {
22863
+ cachedRootSafeAreaTop = null;
22864
+ cacheInvalidationScheduled$1 = false;
22865
+ });
22866
+ }
22867
+ return value;
22868
+ };
22869
+ /**
22870
+ * Returns the initial safe-area configuration based on modal type.
22871
+ * This is called before animation starts and uses configuration-based prediction.
22872
+ *
22873
+ * @param context - Modal context information
22874
+ * @returns SafeAreaConfig with initial safe-area values
22875
+ */
22876
+ const getInitialSafeAreaConfig = (context) => {
22877
+ const { isSheetModal, isCardModal } = context;
22878
+ // Sheet modals always zero top safe-area. The sheet height offset from the
22879
+ // top edge is handled by --ion-modal-offset-top (set in modal.tsx) using
22880
+ // the resolved root value, so --ion-safe-area-top is never needed for
22881
+ // height calculation. Keeping it at 0px prevents header content from
22882
+ // getting double-offset padding.
22883
+ if (isSheetModal) {
22884
+ return {
22885
+ top: '0px',
22886
+ bottom: 'inherit',
22887
+ left: '0px',
22888
+ right: '0px',
22889
+ };
22890
+ }
22891
+ // Card modals need safe-area for height calculation.
22892
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
22893
+ if (isCardModal) {
22894
+ return {
22895
+ top: 'inherit',
22896
+ bottom: 'inherit',
22897
+ left: '0px',
22898
+ right: '0px',
22899
+ };
22900
+ }
22901
+ // On viewports that meet the centered dialog media query breakpoints,
22902
+ // regular modals render as centered dialogs (not fullscreen), so they
22903
+ // don't touch any screen edges and don't need safe-area insets.
22904
+ if (isCenteredDialogViewport()) {
22905
+ return {
22906
+ top: '0px',
22907
+ bottom: '0px',
22908
+ left: '0px',
22909
+ right: '0px',
22910
+ };
22911
+ }
22912
+ // Fullscreen modals on phone - inherit all safe areas
22913
+ return {
22914
+ top: 'inherit',
22915
+ bottom: 'inherit',
22916
+ left: 'inherit',
22917
+ right: 'inherit',
22918
+ };
22919
+ };
22920
+ /**
22921
+ * Returns safe-area configuration based on actual modal position.
22922
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
22923
+ *
22924
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
22925
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
22926
+ * prediction instead. Regular modals use coordinate detection which works reliably
22927
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
22928
+ * once a reliable detection mechanism is available.
22929
+ *
22930
+ * @param wrapperEl - The modal wrapper element to measure
22931
+ * @returns SafeAreaConfig based on position
22932
+ */
22933
+ const getPositionBasedSafeAreaConfig = (wrapperEl) => {
22934
+ var _a, _b;
22935
+ const rect = wrapperEl.getBoundingClientRect();
22936
+ const vh = (_a = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerHeight) !== null && _a !== void 0 ? _a : 0;
22937
+ const vw = (_b = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerWidth) !== null && _b !== void 0 ? _b : 0;
22938
+ // Only apply safe-area to sides where modal overlaps with screen edge
22939
+ return {
22940
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
22941
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
22942
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
22943
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
22944
+ };
22945
+ };
22946
+ /**
22947
+ * Applies safe-area CSS custom property overrides to the modal host element.
22948
+ *
22949
+ * @param hostEl - The modal host element (ion-modal)
22950
+ * @param config - Safe-area configuration to apply
22951
+ */
22952
+ const applySafeAreaOverrides = (hostEl, config) => {
22953
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
22954
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
22955
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
22956
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
22957
+ };
22958
+ /**
22959
+ * Clears safe-area CSS custom property overrides from the modal host element.
22960
+ *
22961
+ * @param hostEl - The modal host element (ion-modal)
22962
+ */
22963
+ const clearSafeAreaOverrides = (hostEl) => {
22964
+ hostEl.style.removeProperty('--ion-safe-area-top');
22965
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
22966
+ hostEl.style.removeProperty('--ion-safe-area-left');
22967
+ hostEl.style.removeProperty('--ion-safe-area-right');
22968
+ };
22969
+
22970
+ 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-modal-offset-top, 0px) + 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
22971
 
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}";
22972
+ 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-modal-offset-top, 0px) + 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
22973
 
22813
22974
  // TODO(FW-2832): types
22814
22975
  /**
@@ -23031,13 +23192,31 @@ class Modal {
23031
23192
  }
23032
23193
  }
23033
23194
  onWindowResize() {
23034
- // Only handle resize for iOS card modals when no custom animations are provided
23035
- if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
23195
+ if (!this.presented)
23036
23196
  return;
23037
- }
23038
23197
  clearTimeout(this.resizeTimeout);
23039
23198
  this.resizeTimeout = setTimeout(() => {
23040
- this.handleViewTransition();
23199
+ const context = this.getSafeAreaContext();
23200
+ // iOS card modals: handle portrait/landscape view transitions
23201
+ if (context.isCardModal && !this.enterAnimation && !this.leaveAnimation) {
23202
+ this.handleViewTransition();
23203
+ }
23204
+ // Sheet modals: re-compute the internal offset property since safe-area
23205
+ // values may change on device rotation (e.g., portrait notch vs landscape).
23206
+ if (context.isSheetModal) {
23207
+ this.updateSheetOffsetTop();
23208
+ }
23209
+ // Regular (non-sheet, non-card) modals: update safe-area overrides
23210
+ // since the viewport may have crossed the centered-dialog breakpoint.
23211
+ if (!context.isSheetModal && !context.isCardModal) {
23212
+ this.updateSafeAreaOverrides();
23213
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
23214
+ if (this.wrapperEl) {
23215
+ this.wrapperEl.style.removeProperty('height');
23216
+ this.wrapperEl.style.removeProperty('padding-bottom');
23217
+ }
23218
+ this.applyFullscreenSafeArea();
23219
+ }
23041
23220
  }, 50); // Debounce to avoid excessive calls during active resizing
23042
23221
  }
23043
23222
  breakpointsChanged(breakpoints) {
@@ -23054,6 +23233,9 @@ class Modal {
23054
23233
  this.triggerController.removeClickListener();
23055
23234
  this.cleanupViewTransitionListener();
23056
23235
  this.cleanupParentRemovalObserver();
23236
+ // Also called in dismiss() — intentional dual cleanup covers both
23237
+ // dismiss-then-remove and direct DOM removal without dismiss.
23238
+ this.cleanupSafeAreaOverrides();
23057
23239
  }
23058
23240
  componentWillLoad() {
23059
23241
  var _a;
@@ -23214,6 +23396,11 @@ class Modal {
23214
23396
  await waitForMount();
23215
23397
  }
23216
23398
  writeTask(() => this.el.classList.add('show-modal'));
23399
+ // Recalculate isSheetModal before safe-area setup because framework
23400
+ // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
23401
+ this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23402
+ // Set initial safe-area overrides before animation
23403
+ this.setInitialSafeAreaOverrides();
23217
23404
  const hasCardModal = presentingElement !== undefined;
23218
23405
  /**
23219
23406
  * We need to change the status bar at the
@@ -23231,6 +23418,10 @@ class Modal {
23231
23418
  backdropBreakpoint: this.backdropBreakpoint,
23232
23419
  expandToScroll: this.expandToScroll,
23233
23420
  });
23421
+ // Update safe-area based on actual position after animation
23422
+ this.updateSafeAreaOverrides();
23423
+ // Apply fullscreen safe-area padding if needed
23424
+ this.applyFullscreenSafeArea();
23234
23425
  /* tslint:disable-next-line */
23235
23426
  if (typeof window !== 'undefined') {
23236
23427
  /**
@@ -23262,13 +23453,7 @@ class Modal {
23262
23453
  };
23263
23454
  window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
23264
23455
  }
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) {
23456
+ if (this.isSheetModal) {
23272
23457
  this.initSheetGesture();
23273
23458
  }
23274
23459
  else if (hasCardModal) {
@@ -23458,6 +23643,9 @@ class Modal {
23458
23643
  if (this.gestureAnimationDismissing && role !== GESTURE) {
23459
23644
  return false;
23460
23645
  }
23646
+ // Cancel any pending resize timeout to prevent stale updates during dismiss
23647
+ clearTimeout(this.resizeTimeout);
23648
+ this.resizeTimeout = undefined;
23461
23649
  /**
23462
23650
  * Because the canDismiss check below is async,
23463
23651
  * we need to claim a lock before the check happens,
@@ -23512,6 +23700,7 @@ class Modal {
23512
23700
  }
23513
23701
  this.cleanupViewTransitionListener();
23514
23702
  this.cleanupParentRemovalObserver();
23703
+ this.cleanupSafeAreaOverrides();
23515
23704
  this.cleanupChildRoutePassthrough();
23516
23705
  }
23517
23706
  this.currentBreakpoint = undefined;
@@ -23632,6 +23821,9 @@ class Modal {
23632
23821
  this.viewTransitionAnimation = transitionAnimation;
23633
23822
  transitionAnimation.play().then(() => {
23634
23823
  this.viewTransitionAnimation = undefined;
23824
+ // Wait for a layout pass after the transition so getBoundingClientRect()
23825
+ // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
23826
+ raf(() => this.updateSafeAreaOverrides());
23635
23827
  // After orientation transition, recreate the swipe-to-close gesture
23636
23828
  // with updated animation that reflects the new presenting element state
23637
23829
  this.reinitSwipeToClose();
@@ -23758,6 +23950,123 @@ class Modal {
23758
23950
  (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
23759
23951
  this.parentRemovalObserver = undefined;
23760
23952
  }
23953
+ /**
23954
+ * Creates the context object for safe-area utilities.
23955
+ */
23956
+ getSafeAreaContext() {
23957
+ return {
23958
+ isSheetModal: this.isSheetModal,
23959
+ isCardModal: this.presentingElement !== undefined && getIonMode$1(this) === 'ios',
23960
+ presentingElement: this.presentingElement,
23961
+ breakpoints: this.breakpoints,
23962
+ currentBreakpoint: this.currentBreakpoint,
23963
+ };
23964
+ }
23965
+ /**
23966
+ * Sets initial safe-area overrides before modal animation.
23967
+ * Called in present() before animation starts.
23968
+ *
23969
+ * For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
23970
+ * (an internal property) instead of --ion-safe-area-top. We resolve the
23971
+ * root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
23972
+ * the height calculation from --ion-safe-area-top (which is zeroed for
23973
+ * sheets to prevent header content from getting double-offset padding).
23974
+ */
23975
+ setInitialSafeAreaOverrides() {
23976
+ const context = this.getSafeAreaContext();
23977
+ const safeAreaConfig = getInitialSafeAreaConfig(context);
23978
+ applySafeAreaOverrides(this.el, safeAreaConfig);
23979
+ // Set the internal offset property with the resolved root safe-area-top value
23980
+ if (context.isSheetModal) {
23981
+ this.updateSheetOffsetTop();
23982
+ }
23983
+ }
23984
+ /**
23985
+ * Resolves the current root --ion-safe-area-top value and sets the
23986
+ * internal --ion-modal-offset-top property on the host element.
23987
+ * Called on present and on resize (e.g., device rotation changes safe-area).
23988
+ */
23989
+ updateSheetOffsetTop() {
23990
+ const safeAreaTop = getRootSafeAreaTop();
23991
+ this.el.style.setProperty('--ion-modal-offset-top', `${safeAreaTop}px`);
23992
+ }
23993
+ /**
23994
+ * Updates safe-area overrides during dynamic state changes.
23995
+ * Called after animations, during gestures, and on orientation changes.
23996
+ */
23997
+ updateSafeAreaOverrides() {
23998
+ const { wrapperEl, el } = this;
23999
+ const context = this.getSafeAreaContext();
24000
+ // Sheet modals: safe-area is fully determined at presentation time
24001
+ // (top is always 0px, height is frozen). Nothing to update.
24002
+ if (context.isSheetModal)
24003
+ return;
24004
+ // Card modals have fixed safe-area requirements set by initial prediction.
24005
+ if (context.isCardModal)
24006
+ return;
24007
+ // wrapperEl is required for position-based detection below
24008
+ if (!wrapperEl)
24009
+ return;
24010
+ // Regular modals: use position-based detection to correctly handle both
24011
+ // fullscreen modals and centered dialogs with custom dimensions.
24012
+ const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
24013
+ applySafeAreaOverrides(el, safeAreaConfig);
24014
+ }
24015
+ /**
24016
+ * Applies padding-bottom to fullscreen modal wrapper to prevent
24017
+ * content from overlapping system navigation bar.
24018
+ */
24019
+ applyFullscreenSafeArea() {
24020
+ const { wrapperEl, el } = this;
24021
+ if (!wrapperEl)
24022
+ return;
24023
+ const context = this.getSafeAreaContext();
24024
+ if (context.isSheetModal || context.isCardModal)
24025
+ return;
24026
+ // Check for standard Ionic layout children (ion-content, ion-footer),
24027
+ // searching one level deep for wrapped components (e.g.,
24028
+ // <app-footer><ion-footer>...</ion-footer></app-footer>).
24029
+ // Note: uses a manual loop instead of querySelector(':scope > ...') because
24030
+ // Stencil's mock-doc (used in spec tests) does not support :scope.
24031
+ let hasContent = false;
24032
+ let hasFooter = false;
24033
+ for (const child of Array.from(el.children)) {
24034
+ if (child.tagName === 'ION-CONTENT')
24035
+ hasContent = true;
24036
+ if (child.tagName === 'ION-FOOTER')
24037
+ hasFooter = true;
24038
+ for (const grandchild of Array.from(child.children)) {
24039
+ if (grandchild.tagName === 'ION-CONTENT')
24040
+ hasContent = true;
24041
+ if (grandchild.tagName === 'ION-FOOTER')
24042
+ hasFooter = true;
24043
+ }
24044
+ }
24045
+ // Only apply wrapper padding for standard Ionic layouts (has ion-content
24046
+ // but no ion-footer). Custom modals with raw HTML are fully
24047
+ // developer-controlled and should not be modified.
24048
+ if (!hasContent || hasFooter)
24049
+ return;
24050
+ // Reduce wrapper height by safe-area and add equivalent padding so the
24051
+ // total visual size stays the same but the flex content area shrinks.
24052
+ // Using height + padding instead of box-sizing: border-box avoids
24053
+ // breaking custom modals that set --border-width (border-box would
24054
+ // include the border inside the height, changing the layout).
24055
+ wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
24056
+ wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
24057
+ }
24058
+ /**
24059
+ * Clears all safe-area overrides and padding from wrapper.
24060
+ */
24061
+ cleanupSafeAreaOverrides() {
24062
+ clearSafeAreaOverrides(this.el);
24063
+ // Remove internal sheet offset property
24064
+ this.el.style.removeProperty('--ion-modal-offset-top');
24065
+ if (this.wrapperEl) {
24066
+ this.wrapperEl.style.removeProperty('height');
24067
+ this.wrapperEl.style.removeProperty('padding-bottom');
24068
+ }
24069
+ }
23761
24070
  render() {
23762
24071
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
23763
24072
  const showHandle = handle !== false && isSheetModal;
@@ -23765,20 +24074,20 @@ class Modal {
23765
24074
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23766
24075
  const isHandleCycle = handleBehavior === 'cycle';
23767
24076
  const isSheetModalWithHandle = isSheetModal && showHandle;
23768
- return (hAsync(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
24077
+ return (hAsync(Host, Object.assign({ key: '038ae26c2cb040043c7d0c217a4de6a7ae909235', "no-router": true,
23769
24078
  // Allow the modal to be navigable when the handle is focusable
23770
24079
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23771
24080
  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',
24081
+ }, 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: '6bab796467b370e79b24ce17c5a3d51adb07ee8c', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'b9a19e86a2c7f079c88ef7a914c7c890aa601887', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'f4315c34aa78be4e3540361755964be2ddf43265',
23773
24082
  /*
23774
24083
  role and aria-modal must be used on the
23775
24084
  same element. They must also be set inside the
23776
24085
  shadow DOM otherwise ion-button will not be highlighted
23777
24086
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23778
24087
  */
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",
24088
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '3f7792bd65e0f8634e62bf27f9edcb636a03030a', class: "modal-handle",
23780
24089
  // 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 }))));
24090
+ 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: '001e5b01831e240bd98f75ff62446b90b64d8394', onSlotchange: this.onSlotChange }))));
23782
24091
  }
23783
24092
  get el() { return getElement(this); }
23784
24093
  static get watchers() { return {
@@ -26712,6 +27021,58 @@ class PickerColumnOption {
26712
27021
  }; }
26713
27022
  }
26714
27023
 
27024
+ /**
27025
+ * Shared per-frame cache for safe-area insets. Avoids creating a temporary
27026
+ * DOM element and forcing a synchronous reflow on every call within the same
27027
+ * frame (e.g., multiple popovers presenting simultaneously). Invalidated
27028
+ * after the next animation frame so values stay fresh across orientation
27029
+ * changes and viewport resizes.
27030
+ */
27031
+ let cachedInsets = null;
27032
+ let cacheInvalidationScheduled = false;
27033
+ /**
27034
+ * Resolves the current --ion-safe-area-* CSS custom property values
27035
+ * to actual pixel numbers. A temporary element is needed because
27036
+ * getComputedStyle on :root returns the declared value of custom
27037
+ * properties (e.g. "env(safe-area-inset-top)") rather than a
27038
+ * resolved number. By assigning them to standard padding properties,
27039
+ * the browser resolves the value.
27040
+ *
27041
+ * Results are cached for the current frame to avoid repeated reflows.
27042
+ */
27043
+ const getSafeAreaInsets = (doc) => {
27044
+ if (cachedInsets !== null) {
27045
+ return cachedInsets;
27046
+ }
27047
+ if (doc.body === null) {
27048
+ return { top: 0, bottom: 0, left: 0, right: 0 };
27049
+ }
27050
+ const el = doc.createElement('div');
27051
+ el.style.cssText =
27052
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
27053
+ 'padding-top:var(--ion-safe-area-top,0px);' +
27054
+ 'padding-bottom:var(--ion-safe-area-bottom,0px);' +
27055
+ 'padding-left:var(--ion-safe-area-left,0px);' +
27056
+ 'padding-right:var(--ion-safe-area-right,0px);';
27057
+ doc.body.appendChild(el);
27058
+ const style = getComputedStyle(el);
27059
+ const insets = {
27060
+ top: parseFloat(style.paddingTop) || 0,
27061
+ bottom: parseFloat(style.paddingBottom) || 0,
27062
+ left: parseFloat(style.paddingLeft) || 0,
27063
+ right: parseFloat(style.paddingRight) || 0,
27064
+ };
27065
+ el.remove();
27066
+ cachedInsets = insets;
27067
+ if (!cacheInvalidationScheduled) {
27068
+ cacheInvalidationScheduled = true;
27069
+ raf(() => {
27070
+ cachedInsets = null;
27071
+ cacheInvalidationScheduled = false;
27072
+ });
27073
+ }
27074
+ return insets;
27075
+ };
26715
27076
  /**
26716
27077
  * Returns the dimensions of the popover
26717
27078
  * arrow on `ios` mode. If arrow is disabled
@@ -27347,8 +27708,10 @@ const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, con
27347
27708
  * Adjusts popover positioning coordinates
27348
27709
  * such that popover does not appear offscreen
27349
27710
  * or overlapping safe area bounds.
27711
+ *
27712
+ * @internal - This is not part of the public API.
27350
27713
  */
27351
- const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeAreaMargin, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
27714
+ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
27352
27715
  let arrowTop = coordArrowTop;
27353
27716
  const arrowLeft = coordArrowLeft;
27354
27717
  let left = coordLeft;
@@ -27358,16 +27721,19 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27358
27721
  let originY = contentOriginY;
27359
27722
  let checkSafeAreaLeft = false;
27360
27723
  let checkSafeAreaRight = false;
27724
+ let checkSafeAreaTop = false;
27725
+ let checkSafeAreaBottom = false;
27361
27726
  const triggerTop = triggerCoordinates
27362
27727
  ? triggerCoordinates.top + triggerCoordinates.height
27363
27728
  : bodyHeight / 2 - contentHeight / 2;
27364
27729
  const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
27365
27730
  let addPopoverBottomClass = false;
27731
+ const hideArrow = false;
27366
27732
  /**
27367
27733
  * Adjust popover so it does not
27368
27734
  * go off the left of the screen.
27369
27735
  */
27370
- if (left < bodyPadding + safeAreaMargin) {
27736
+ if (left < bodyPadding + safeArea.left) {
27371
27737
  left = bodyPadding;
27372
27738
  checkSafeAreaLeft = true;
27373
27739
  originX = 'left';
@@ -27376,41 +27742,58 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27376
27742
  * go off the right of the screen.
27377
27743
  */
27378
27744
  }
27379
- else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) {
27745
+ else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
27380
27746
  checkSafeAreaRight = true;
27381
27747
  left = bodyWidth - contentWidth - bodyPadding;
27382
27748
  originX = 'right';
27383
27749
  }
27384
27750
  /**
27385
27751
  * Adjust popover so it does not
27386
- * go off the top of the screen.
27752
+ * go off the bottom of the screen
27753
+ * or overlap the bottom safe area.
27387
27754
  * If popover is on the left or the right of
27388
27755
  * the trigger, then we should not adjust top
27389
27756
  * margins.
27390
27757
  */
27391
- if (triggerTop + triggerHeight + contentHeight > bodyHeight && (side === 'top' || side === 'bottom')) {
27392
- if (triggerTop - contentHeight > 0) {
27758
+ if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
27759
+ (side === 'top' || side === 'bottom')) {
27760
+ /**
27761
+ * Calculate where the popover top would be if flipped
27762
+ * above the trigger. Check whether that position clears
27763
+ * the top safe area with room for bodyPadding.
27764
+ */
27765
+ const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
27766
+ if (idealFlipTop >= safeArea.top + bodyPadding) {
27393
27767
  /**
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.
27768
+ * Popover fits above the trigger without overlapping
27769
+ * the top safe area. Use the ideal position directly —
27770
+ * no safe-area CSS vars needed since it clears both edges.
27402
27771
  */
27403
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27772
+ top = idealFlipTop;
27404
27773
  arrowTop = top + contentHeight;
27405
27774
  originY = 'bottom';
27406
27775
  addPopoverBottomClass = true;
27407
- /**
27408
- * If not enough room for popover to appear
27409
- * above trigger, then cut it off.
27410
- */
27411
27776
  }
27412
27777
  else {
27778
+ /**
27779
+ * Can't flip above the trigger. Constrain the bottom
27780
+ * edge while keeping the top near the trigger. This
27781
+ * creates a scrollable area anchored to the trigger.
27782
+ */
27413
27783
  bottom = bodyPadding;
27784
+ checkSafeAreaBottom = true;
27785
+ /**
27786
+ * When the trigger is near the bottom of the screen,
27787
+ * the calculated top may be at or past the bottom
27788
+ * constraint, leaving zero visible height. In that
27789
+ * case, pull top up to the top safe area so the
27790
+ * popover fills the available space between safe areas.
27791
+ */
27792
+ const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
27793
+ if (top >= bottomEdge) {
27794
+ top = safeArea.top + bodyPadding;
27795
+ checkSafeAreaTop = true;
27796
+ }
27414
27797
  }
27415
27798
  }
27416
27799
  return {
@@ -27421,9 +27804,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27421
27804
  originY,
27422
27805
  checkSafeAreaLeft,
27423
27806
  checkSafeAreaRight,
27807
+ checkSafeAreaTop,
27808
+ checkSafeAreaBottom,
27424
27809
  arrowTop,
27425
27810
  arrowLeft,
27426
27811
  addPopoverBottomClass,
27812
+ hideArrow,
27427
27813
  };
27428
27814
  };
27429
27815
  const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
@@ -27453,6 +27839,15 @@ const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
27453
27839
  };
27454
27840
 
27455
27841
  const POPOVER_IOS_BODY_PADDING = 5;
27842
+ /**
27843
+ * Minimum edge margin for iOS popovers ensures visual spacing from screen
27844
+ * edges on devices without safe areas (e.g., older iPhones without notches).
27845
+ * Previously this was a hardcoded `safeAreaMargin = 25` that served dual
27846
+ * purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
27847
+ * insets are read dynamically, this floor preserves the visual spacing when
27848
+ * safe-area values are 0.
27849
+ */
27850
+ const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
27456
27851
  /**
27457
27852
  * iOS Popover Enter Animation
27458
27853
  */
@@ -27478,8 +27873,16 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27478
27873
  };
27479
27874
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27480
27875
  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);
27876
+ const rawSafeArea = getSafeAreaInsets(doc);
27877
+ const safeArea = size === 'cover'
27878
+ ? { top: 0, bottom: 0, left: 0, right: 0 }
27879
+ : {
27880
+ top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
27881
+ bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
27882
+ left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
27883
+ right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
27884
+ };
27885
+ 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
27886
  const baseAnimation = createAnimation();
27484
27887
  const backdropAnimation = createAnimation();
27485
27888
  const contentAnimation = createAnimation();
@@ -27510,10 +27913,14 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27510
27913
  baseEl.classList.add('popover-bottom');
27511
27914
  }
27512
27915
  if (bottom !== undefined) {
27513
- contentEl.style.setProperty('bottom', `${bottom}px`);
27916
+ let bottomValue = `${bottom}px`;
27917
+ if (checkSafeAreaBottom) {
27918
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
27919
+ }
27920
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27514
27921
  }
27515
- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27516
- const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27922
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
27923
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
27517
27924
  let leftValue = `${left}px`;
27518
27925
  if (checkSafeAreaLeft) {
27519
27926
  leftValue = `${left}px${safeAreaLeft}`;
@@ -27521,7 +27928,11 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27521
27928
  if (checkSafeAreaRight) {
27522
27929
  leftValue = `${left}px${safeAreaRight}`;
27523
27930
  }
27524
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27931
+ let topValue = `${top}px`;
27932
+ if (checkSafeAreaTop) {
27933
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
27934
+ }
27935
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27525
27936
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27526
27937
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27527
27938
  if (arrowEl !== null) {
@@ -27597,7 +28008,23 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27597
28008
  };
27598
28009
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27599
28010
  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);
28011
+ // MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
28012
+ // This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
28013
+ const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
28014
+ 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);
28015
+ const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
28016
+ const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
28017
+ let leftValue = `${left}px`;
28018
+ if (checkSafeAreaLeft) {
28019
+ leftValue = `${left}px${safeAreaLeftCalc}`;
28020
+ }
28021
+ if (checkSafeAreaRight) {
28022
+ leftValue = `${left}px${safeAreaRightCalc}`;
28023
+ }
28024
+ let topValue = `${top}px`;
28025
+ if (checkSafeAreaTop) {
28026
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
28027
+ }
27601
28028
  const baseAnimation = createAnimation();
27602
28029
  const backdropAnimation = createAnimation();
27603
28030
  const wrapperAnimation = createAnimation();
@@ -27614,13 +28041,17 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27614
28041
  contentAnimation
27615
28042
  .addElement(contentEl)
27616
28043
  .beforeStyles({
27617
- top: `calc(${top}px + var(--offset-y, 0px))`,
27618
- left: `calc(${left}px + var(--offset-x, 0px))`,
28044
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
28045
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
27619
28046
  'transform-origin': `${originY} ${originX}`,
27620
28047
  })
27621
28048
  .beforeAddWrite(() => {
27622
28049
  if (bottom !== undefined) {
27623
- contentEl.style.setProperty('bottom', `${bottom}px`);
28050
+ let bottomValue = `${bottom}px`;
28051
+ if (checkSafeAreaBottom) {
28052
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
28053
+ }
28054
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27624
28055
  }
27625
28056
  })
27626
28057
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -27632,7 +28063,7 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27632
28063
  if (size === 'cover') {
27633
28064
  baseEl.style.setProperty('--width', `${contentWidth}px`);
27634
28065
  }
27635
- if (originY === 'bottom') {
28066
+ if (addPopoverBottomClass) {
27636
28067
  baseEl.classList.add('popover-bottom');
27637
28068
  }
27638
28069
  })
@@ -29737,8 +30168,6 @@ class Refresher {
29737
30168
  this.ionRefresh = createEvent(this, "ionRefresh", 7);
29738
30169
  this.ionPull = createEvent(this, "ionPull", 7);
29739
30170
  this.ionStart = createEvent(this, "ionStart", 7);
29740
- this.ionPullStart = createEvent(this, "ionPullStart", 7);
29741
- this.ionPullEnd = createEvent(this, "ionPullEnd", 7);
29742
30171
  this.appliedStyles = false;
29743
30172
  this.didStart = false;
29744
30173
  this.progress = 0;
@@ -29793,8 +30222,8 @@ class Refresher {
29793
30222
  * than `1`. The default value is `1` which is equal to the speed of the cursor.
29794
30223
  * If a negative value is passed in, the factor will be `1` instead.
29795
30224
  *
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
30225
+ * For example: If the value passed is `1.2` and the content is dragged by
30226
+ * `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
29798
30227
  * (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
29799
30228
  * will be `8` pixels, less than the amount the cursor has moved.
29800
30229
  *
@@ -29844,9 +30273,6 @@ class Refresher {
29844
30273
  this.animations = [];
29845
30274
  this.progress = 0;
29846
30275
  this.state = 1 /* RefresherState.Inactive */;
29847
- this.ionPullEnd.emit({
29848
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
29849
- });
29850
30276
  }
29851
30277
  async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
29852
30278
  this.elementToTransform = this.scrollEl;
@@ -29879,7 +30305,6 @@ class Refresher {
29879
30305
  if (!this.didStart) {
29880
30306
  this.didStart = true;
29881
30307
  this.ionStart.emit();
29882
- this.ionPullStart.emit();
29883
30308
  }
29884
30309
  // emit "pulling" on every move
29885
30310
  if (this.pointerDown) {
@@ -29955,7 +30380,6 @@ class Refresher {
29955
30380
  this.lastVelocityY = ev.velocityY;
29956
30381
  },
29957
30382
  onEnd: () => {
29958
- const hadStarted = this.didStart;
29959
30383
  this.pointerDown = false;
29960
30384
  this.didStart = false;
29961
30385
  if (this.needsCompletion) {
@@ -29965,13 +30389,6 @@ class Refresher {
29965
30389
  else if (this.didRefresh) {
29966
30390
  readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
29967
30391
  }
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
30392
  },
29976
30393
  });
29977
30394
  this.disabledChanged();
@@ -30018,7 +30435,6 @@ class Refresher {
30018
30435
  ev.data.animation = animation;
30019
30436
  animation.progressStart(false, 0);
30020
30437
  this.ionStart.emit();
30021
- this.ionPullStart.emit();
30022
30438
  this.animations.push(animation);
30023
30439
  return;
30024
30440
  }
@@ -30041,7 +30457,6 @@ class Refresher {
30041
30457
  this.animations = [];
30042
30458
  this.gesture.enable(true);
30043
30459
  this.state = 1 /* RefresherState.Inactive */;
30044
- this.ionPullEnd.emit({ reason: 'cancel' });
30045
30460
  });
30046
30461
  return;
30047
30462
  }
@@ -30286,7 +30701,6 @@ class Refresher {
30286
30701
  if (!this.didStart) {
30287
30702
  this.didStart = true;
30288
30703
  this.ionStart.emit();
30289
- this.ionPullStart.emit();
30290
30704
  }
30291
30705
  // emit "pulling" on every move
30292
30706
  this.ionPull.emit();
@@ -30330,15 +30744,6 @@ class Refresher {
30330
30744
  * available right away.
30331
30745
  */
30332
30746
  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
30747
  }
30343
30748
  }
30344
30749
  beginRefresh() {
@@ -30385,9 +30790,6 @@ class Refresher {
30385
30790
  if (this.contentFullscreen && this.backgroundContentEl) {
30386
30791
  (_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
30387
30792
  }
30388
- this.ionPullEnd.emit({
30389
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
30390
- });
30391
30793
  }, 600);
30392
30794
  // reset the styles on the scroll element
30393
30795
  // set that the refresh is actively cancelling/completing
@@ -30441,7 +30843,7 @@ class Refresher {
30441
30843
  }
30442
30844
  render() {
30443
30845
  const mode = getIonMode$1(this);
30444
- return (hAsync(Host, { key: 'bed0c2f44c32ecac24b7d8326ac7c7ef1d12c44f', slot: "fixed", class: {
30846
+ return (hAsync(Host, { key: '2d1bd880877b698604542ab2d602d38b9504d975', slot: "fixed", class: {
30445
30847
  [mode]: true,
30446
30848
  // Used internally for styling
30447
30849
  [`refresher-${mode}`]: true,