@ionic/core 8.7.18-dev.11769790854.11895f8f → 8.7.18-dev.11770316329.126ef4af

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 (38) hide show
  1. package/components/ion-refresher.js +32 -3
  2. package/components/modal.js +12 -220
  3. package/components/popover.js +11 -83
  4. package/dist/cjs/ion-modal.cjs.entry.js +12 -220
  5. package/dist/cjs/ion-popover.cjs.entry.js +11 -83
  6. package/dist/cjs/ion-refresher_2.cjs.entry.js +32 -3
  7. package/dist/collection/components/modal/gestures/sheet.js +1 -3
  8. package/dist/collection/components/modal/gestures/swipe-to-close.js +1 -3
  9. package/dist/collection/components/modal/modal.ios.css +4 -0
  10. package/dist/collection/components/modal/modal.js +7 -215
  11. package/dist/collection/components/modal/modal.md.css +4 -0
  12. package/dist/collection/components/popover/animations/ios.enter.js +5 -21
  13. package/dist/collection/components/popover/animations/md.enter.js +5 -30
  14. package/dist/collection/components/popover/utils.js +1 -32
  15. package/dist/collection/components/refresher/refresher.js +70 -4
  16. package/dist/docs.json +49 -4
  17. package/dist/esm/ion-modal.entry.js +12 -220
  18. package/dist/esm/ion-popover.entry.js +11 -83
  19. package/dist/esm/ion-refresher_2.entry.js +32 -3
  20. package/dist/html.html-data.json +1 -1
  21. package/dist/ionic/ionic.esm.js +1 -1
  22. package/dist/ionic/p-90f2d3ff.entry.js +4 -0
  23. package/dist/ionic/p-91840a80.entry.js +4 -0
  24. package/dist/ionic/p-f9061316.entry.js +4 -0
  25. package/dist/types/components/modal/gestures/sheet.d.ts +1 -1
  26. package/dist/types/components/modal/gestures/swipe-to-close.d.ts +1 -1
  27. package/dist/types/components/modal/modal.d.ts +0 -47
  28. package/dist/types/components/popover/utils.d.ts +0 -2
  29. package/dist/types/components/refresher/refresher-interface.d.ts +7 -0
  30. package/dist/types/components/refresher/refresher.d.ts +16 -3
  31. package/dist/types/components.d.ts +16 -5
  32. package/dist/types/interface.d.ts +1 -1
  33. package/hydrate/index.js +54 -305
  34. package/hydrate/index.mjs +54 -305
  35. package/package.json +2 -4
  36. package/dist/ionic/p-639dd543.entry.js +0 -4
  37. package/dist/ionic/p-ec9ca3fe.entry.js +0 -4
  38. package/dist/ionic/p-ef4256eb.entry.js +0 -4
@@ -660,8 +660,6 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
660
660
  let bottom;
661
661
  let originX = contentOriginX;
662
662
  let originY = contentOriginY;
663
- let checkSafeAreaTop = false;
664
- let checkSafeAreaBottom = false;
665
663
  let checkSafeAreaLeft = false;
666
664
  let checkSafeAreaRight = false;
667
665
  const triggerTop = triggerCoordinates
@@ -706,18 +704,10 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
706
704
  * We chose 12 here so that the popover position looks a bit nicer as
707
705
  * it is not right up against the edge of the screen.
708
706
  */
709
- top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
707
+ top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
710
708
  arrowTop = top + contentHeight;
711
709
  originY = 'bottom';
712
710
  addPopoverBottomClass = true;
713
- /**
714
- * If the popover is positioned near the top edge, account for safe area.
715
- * This ensures the popover doesn't overlap with status bars or notches.
716
- */
717
- if (top <= bodyPadding + safeAreaMargin) {
718
- checkSafeAreaTop = true;
719
- top = bodyPadding;
720
- }
721
711
  /**
722
712
  * If not enough room for popover to appear
723
713
  * above trigger, then cut it off.
@@ -725,35 +715,14 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
725
715
  }
726
716
  else {
727
717
  bottom = bodyPadding;
728
- /**
729
- * When the popover is pinned to the bottom, account for safe area.
730
- * This ensures the popover doesn't overlap with home indicators
731
- * or navigation bars (e.g., Android API 36+ edge-to-edge).
732
- */
733
- checkSafeAreaBottom = true;
734
718
  }
735
719
  }
736
- /**
737
- * Final check: If the popover extends into any safe-area region,
738
- * ensure the corresponding flag is set regardless of side.
739
- * This handles cases where a side-positioned popover (left/right)
740
- * still needs bottom safe-area padding because it extends into that region.
741
- */
742
- const popoverBottom = bottom !== undefined ? bodyHeight - bottom : top + contentHeight;
743
- if (popoverBottom + safeAreaMargin > bodyHeight) {
744
- checkSafeAreaBottom = true;
745
- }
746
- if (top < safeAreaMargin) {
747
- checkSafeAreaTop = true;
748
- }
749
720
  return {
750
721
  top,
751
722
  left,
752
723
  bottom,
753
724
  originX,
754
725
  originY,
755
- checkSafeAreaTop,
756
- checkSafeAreaBottom,
757
726
  checkSafeAreaLeft,
758
727
  checkSafeAreaRight,
759
728
  arrowTop,
@@ -814,7 +783,7 @@ const iosEnterAnimation = (baseEl, opts) => {
814
783
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
815
784
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
816
785
  const margin = size === 'cover' ? 0 : 25;
817
- const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, 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);
786
+ 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);
818
787
  const baseAnimation = animation.createAnimation();
819
788
  const backdropAnimation = animation.createAnimation();
820
789
  const contentAnimation = animation.createAnimation();
@@ -844,35 +813,19 @@ const iosEnterAnimation = (baseEl, opts) => {
844
813
  if (addPopoverBottomClass) {
845
814
  baseEl.classList.add('popover-bottom');
846
815
  }
847
- /**
848
- * Safe area CSS variable adjustments.
849
- * When the popover is positioned near an edge, we add the corresponding
850
- * safe-area inset to ensure the popover doesn't overlap with system UI
851
- * (status bars, home indicators, navigation bars on Android API 36+, etc.)
852
- */
853
- const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
854
- const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
816
+ if (bottom !== undefined) {
817
+ contentEl.style.setProperty('bottom', `${bottom}px`);
818
+ }
855
819
  const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
856
820
  const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
857
- let topValue = `${top}px`;
858
- let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
859
821
  let leftValue = `${left}px`;
860
- if (checkSafeAreaTop) {
861
- topValue = `${top}px${safeAreaTop}`;
862
- }
863
- if (checkSafeAreaBottom && bottomValue !== undefined) {
864
- bottomValue = `${bottom}px${safeAreaBottom}`;
865
- }
866
822
  if (checkSafeAreaLeft) {
867
823
  leftValue = `${left}px${safeAreaLeft}`;
868
824
  }
869
825
  if (checkSafeAreaRight) {
870
826
  leftValue = `${left}px${safeAreaRight}`;
871
827
  }
872
- if (bottomValue !== undefined) {
873
- contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
874
- }
875
- contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
828
+ contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
876
829
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
877
830
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
878
831
  if (arrowEl !== null) {
@@ -948,32 +901,7 @@ const mdEnterAnimation = (baseEl, opts) => {
948
901
  };
949
902
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
950
903
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
951
- const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom, checkSafeAreaLeft, checkSafeAreaRight, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
952
- /**
953
- * Safe area CSS variable adjustments.
954
- * When the popover is positioned near an edge, we add the corresponding
955
- * safe-area inset to ensure the popover doesn't overlap with system UI
956
- * (status bars, home indicators, navigation bars on Android API 36+, etc.)
957
- */
958
- const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
959
- const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
960
- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
961
- const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
962
- let topValue = `${top}px`;
963
- let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
964
- let leftValue = `${left}px`;
965
- if (checkSafeAreaTop) {
966
- topValue = `${top}px${safeAreaTop}`;
967
- }
968
- if (checkSafeAreaBottom && bottomValue !== undefined) {
969
- bottomValue = `${bottom}px${safeAreaBottom}`;
970
- }
971
- if (checkSafeAreaLeft) {
972
- leftValue = `${left}px${safeAreaLeft}`;
973
- }
974
- if (checkSafeAreaRight) {
975
- leftValue = `${left}px${safeAreaRight}`;
976
- }
904
+ const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
977
905
  const baseAnimation = animation.createAnimation();
978
906
  const backdropAnimation = animation.createAnimation();
979
907
  const wrapperAnimation = animation.createAnimation();
@@ -990,13 +918,13 @@ const mdEnterAnimation = (baseEl, opts) => {
990
918
  contentAnimation
991
919
  .addElement(contentEl)
992
920
  .beforeStyles({
993
- top: `calc(${topValue} + var(--offset-y, 0px))`,
994
- left: `calc(${leftValue} + var(--offset-x, 0px))`,
921
+ top: `calc(${top}px + var(--offset-y, 0px))`,
922
+ left: `calc(${left}px + var(--offset-x, 0px))`,
995
923
  'transform-origin': `${originY} ${originX}`,
996
924
  })
997
925
  .beforeAddWrite(() => {
998
- if (bottomValue !== undefined) {
999
- contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
926
+ if (bottom !== undefined) {
927
+ contentEl.style.setProperty('bottom', `${bottom}px`);
1000
928
  }
1001
929
  })
1002
930
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -214,6 +214,8 @@ const Refresher = class {
214
214
  this.ionRefresh = index.createEvent(this, "ionRefresh", 7);
215
215
  this.ionPull = index.createEvent(this, "ionPull", 7);
216
216
  this.ionStart = index.createEvent(this, "ionStart", 7);
217
+ this.ionPullStart = index.createEvent(this, "ionPullStart", 7);
218
+ this.ionPullEnd = index.createEvent(this, "ionPullEnd", 7);
217
219
  this.appliedStyles = false;
218
220
  this.didStart = false;
219
221
  this.progress = 0;
@@ -268,8 +270,8 @@ const Refresher = class {
268
270
  * than `1`. The default value is `1` which is equal to the speed of the cursor.
269
271
  * If a negative value is passed in, the factor will be `1` instead.
270
272
  *
271
- * For example: If the value passed is `1.2` and the content is dragged by
272
- * `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
273
+ * For example, If the value passed is `1.2` and the content is dragged by
274
+ * `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels
273
275
  * (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
274
276
  * will be `8` pixels, less than the amount the cursor has moved.
275
277
  *
@@ -319,6 +321,9 @@ const Refresher = class {
319
321
  this.animations = [];
320
322
  this.progress = 0;
321
323
  this.state = 1 /* RefresherState.Inactive */;
324
+ this.ionPullEnd.emit({
325
+ reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
326
+ });
322
327
  }
323
328
  async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
324
329
  this.elementToTransform = this.scrollEl;
@@ -351,6 +356,7 @@ const Refresher = class {
351
356
  if (!this.didStart) {
352
357
  this.didStart = true;
353
358
  this.ionStart.emit();
359
+ this.ionPullStart.emit();
354
360
  }
355
361
  // emit "pulling" on every move
356
362
  if (this.pointerDown) {
@@ -426,6 +432,7 @@ const Refresher = class {
426
432
  this.lastVelocityY = ev.velocityY;
427
433
  },
428
434
  onEnd: () => {
435
+ const hadStarted = this.didStart;
429
436
  this.pointerDown = false;
430
437
  this.didStart = false;
431
438
  if (this.needsCompletion) {
@@ -435,6 +442,13 @@ const Refresher = class {
435
442
  else if (this.didRefresh) {
436
443
  index.readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
437
444
  }
445
+ else if (hadStarted) {
446
+ /**
447
+ * User started pulling but released before reaching the refresh threshold.
448
+ * Emit ionPullEnd to complete the event pair.
449
+ */
450
+ this.ionPullEnd.emit({ reason: 'cancel' });
451
+ }
438
452
  },
439
453
  });
440
454
  this.disabledChanged();
@@ -481,6 +495,7 @@ const Refresher = class {
481
495
  ev.data.animation = animation;
482
496
  animation.progressStart(false, 0);
483
497
  this.ionStart.emit();
498
+ this.ionPullStart.emit();
484
499
  this.animations.push(animation);
485
500
  return;
486
501
  }
@@ -503,6 +518,7 @@ const Refresher = class {
503
518
  this.animations = [];
504
519
  this.gesture.enable(true);
505
520
  this.state = 1 /* RefresherState.Inactive */;
521
+ this.ionPullEnd.emit({ reason: 'cancel' });
506
522
  });
507
523
  return;
508
524
  }
@@ -747,6 +763,7 @@ const Refresher = class {
747
763
  if (!this.didStart) {
748
764
  this.didStart = true;
749
765
  this.ionStart.emit();
766
+ this.ionPullStart.emit();
750
767
  }
751
768
  // emit "pulling" on every move
752
769
  this.ionPull.emit();
@@ -790,6 +807,15 @@ const Refresher = class {
790
807
  * available right away.
791
808
  */
792
809
  this.restoreOverflowStyle();
810
+ /**
811
+ * If ionPullStart was emitted, we need to emit ionPullEnd
812
+ * even though the gesture was aborted before reaching the
813
+ * pulling threshold.
814
+ */
815
+ if (this.didStart) {
816
+ this.didStart = false;
817
+ this.ionPullEnd.emit({ reason: 'cancel' });
818
+ }
793
819
  }
794
820
  }
795
821
  beginRefresh() {
@@ -836,6 +862,9 @@ const Refresher = class {
836
862
  if (this.contentFullscreen && this.backgroundContentEl) {
837
863
  (_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
838
864
  }
865
+ this.ionPullEnd.emit({
866
+ reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
867
+ });
839
868
  }, 600);
840
869
  // reset the styles on the scroll element
841
870
  // set that the refresh is actively cancelling/completing
@@ -889,7 +918,7 @@ const Refresher = class {
889
918
  }
890
919
  render() {
891
920
  const mode = ionicGlobal.getIonMode(this);
892
- return (index.h(index.Host, { key: '2d1bd880877b698604542ab2d602d38b9504d975', slot: "fixed", class: {
921
+ return (index.h(index.Host, { key: 'bed0c2f44c32ecac24b7d8326ac7c7ef1d12c44f', slot: "fixed", class: {
893
922
  [mode]: true,
894
923
  // Used internally for styling
895
924
  [`refresher-${mode}`]: true,
@@ -7,7 +7,7 @@ import { clamp, getElementRoot, raf } from "../../../utils/helpers";
7
7
  import { FOCUS_TRAP_DISABLE_CLASS } from "../../../utils/overlays";
8
8
  import { getBackdropValueForSheet } from "../utils";
9
9
  import { calculateSpringStep, handleCanDismiss } from "./utils";
10
- export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange, onGestureMove) => {
10
+ export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, backdropBreakpoint, animation, breakpoints = [], expandToScroll, getCurrentBreakpoint, onDismiss, onBreakpointChange) => {
11
11
  // Defaults for the sheet swipe animation
12
12
  const defaultBackdrop = [
13
13
  { offset: 0, opacity: 'var(--backdrop-opacity)' },
@@ -338,8 +338,6 @@ export const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpo
338
338
  : step;
339
339
  offset = clamp(0.0001, processedStep, maxStep);
340
340
  animation.progressStep(offset);
341
- // Notify modal of position change for safe-area updates
342
- onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
343
341
  };
344
342
  const onEnd = (detail) => {
345
343
  /**
@@ -12,7 +12,7 @@ import { calculateSpringStep, handleCanDismiss } from "./utils";
12
12
  export const SwipeToCloseDefaults = {
13
13
  MIN_PRESENTING_SCALE: 0.915,
14
14
  };
15
- export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss, onGestureMove) => {
15
+ export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismiss) => {
16
16
  /**
17
17
  * The step value at which a card modal
18
18
  * is eligible for dismissing via gesture.
@@ -169,8 +169,6 @@ export const createSwipeToCloseGesture = (el, animation, statusBarStyle, onDismi
169
169
  const processedStep = isAttemptingDismissWithCanDismiss ? calculateSpringStep(step / maxStep) : step;
170
170
  const clampedStep = clamp(0.0001, processedStep, maxStep);
171
171
  animation.progressStep(clampedStep);
172
- // Notify modal of position change for safe-area updates
173
- onGestureMove === null || onGestureMove === void 0 ? void 0 : onGestureMove();
174
172
  /**
175
173
  * When swiping down half way, the status bar style
176
174
  * should be reset to its default value.
@@ -135,6 +135,10 @@ ion-backdrop {
135
135
  :host {
136
136
  --width: 600px;
137
137
  --height: 500px;
138
+ --ion-safe-area-top: 0px;
139
+ --ion-safe-area-bottom: 0px;
140
+ --ion-safe-area-right: 0px;
141
+ --ion-safe-area-left: 0px;
138
142
  }
139
143
  }
140
144
  @media only screen and (min-width: 768px) and (min-height: 768px) {
@@ -2,7 +2,6 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { Host, h, writeTask } from "@stencil/core";
5
- import { win } from "../../utils/browser/index";
6
5
  import { findIonContent, printIonContentErrorMsg } from "../../utils/content/index";
7
6
  import { CoreDelegate, attachComponent, detachComponent } from "../../utils/framework-delegate";
8
7
  import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from "../../utils/helpers";
@@ -43,10 +42,6 @@ export class Modal {
43
42
  this.inline = false;
44
43
  // Whether or not modal is being dismissed via gesture
45
44
  this.gestureAnimationDismissing = false;
46
- // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
47
- this.skipSafeAreaCoordinateDetection = false;
48
- // Track previous safe-area state to avoid redundant DOM writes
49
- this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
50
45
  this.presented = false;
51
46
  /** @internal */
52
47
  this.hasController = false;
@@ -237,10 +232,7 @@ export class Modal {
237
232
  }
238
233
  }
239
234
  onWindowResize() {
240
- // Invalidate safe-area cache on resize (device rotation may change values)
241
- this.cachedSafeAreas = undefined;
242
- this.updateSafeAreaOverrides();
243
- // Only handle view transition for iOS card modals when no custom animations are provided
235
+ // Only handle resize for iOS card modals when no custom animations are provided
244
236
  if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
245
237
  return;
246
238
  }
@@ -263,8 +255,6 @@ export class Modal {
263
255
  this.triggerController.removeClickListener();
264
256
  this.cleanupViewTransitionListener();
265
257
  this.cleanupParentRemovalObserver();
266
- // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
267
- this.resetSafeAreaState();
268
258
  }
269
259
  componentWillLoad() {
270
260
  var _a;
@@ -424,8 +414,6 @@ export class Modal {
424
414
  else if (!this.keepContentsMounted) {
425
415
  await waitForMount();
426
416
  }
427
- // Predict safe-area needs based on modal configuration to avoid visual snap
428
- this.setInitialSafeAreaOverrides(presentingElement);
429
417
  writeTask(() => this.el.classList.add('show-modal'));
430
418
  const hasCardModal = presentingElement !== undefined;
431
419
  /**
@@ -487,8 +475,6 @@ export class Modal {
487
475
  else if (hasCardModal) {
488
476
  this.initSwipeToClose();
489
477
  }
490
- // Now that animation is complete, update safe-area based on actual position
491
- this.updateSafeAreaOverrides();
492
478
  // Initialize view transition listener for iOS card modals
493
479
  this.initViewTransitionListener();
494
480
  // Initialize parent removal observer
@@ -540,7 +526,7 @@ export class Modal {
540
526
  await this.dismiss(undefined, GESTURE);
541
527
  this.gestureAnimationDismissing = false;
542
528
  });
543
- }, () => this.updateSafeAreaOverrides());
529
+ });
544
530
  this.gesture.enable(true);
545
531
  }
546
532
  initSheetGesture() {
@@ -561,8 +547,7 @@ export class Modal {
561
547
  this.currentBreakpoint = breakpoint;
562
548
  this.ionBreakpointDidChange.emit({ breakpoint });
563
549
  }
564
- this.updateSafeAreaOverrides();
565
- }, () => this.updateSafeAreaOverrides());
550
+ });
566
551
  this.gesture = gesture;
567
552
  this.moveSheetToBreakpoint = moveSheetToBreakpoint;
568
553
  this.gesture.enable(true);
@@ -640,197 +625,6 @@ export class Modal {
640
625
  // Clear the cached reference
641
626
  this.cachedPageParent = undefined;
642
627
  }
643
- /**
644
- * Sets initial safe-area overrides based on modal configuration before
645
- * the modal becomes visible. This predicts whether the modal will touch
646
- * screen edges to avoid a visual snap after animation completes.
647
- */
648
- setInitialSafeAreaOverrides(presentingElement) {
649
- const style = this.el.style;
650
- const mode = getIonMode(this);
651
- const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
652
- // Card modals only exist in iOS mode - in MD mode, presentingElement is ignored
653
- const isCardModal = presentingElement !== undefined && mode === 'ios';
654
- const isTablet = window.innerWidth >= 768;
655
- // Sheet modals always touch bottom edge, never top/left/right
656
- if (isSheetModal) {
657
- style.setProperty('--ion-safe-area-top', '0px');
658
- style.setProperty('--ion-safe-area-left', '0px');
659
- style.setProperty('--ion-safe-area-right', '0px');
660
- return;
661
- }
662
- // Card modals have rounded top corners
663
- if (isCardModal) {
664
- style.setProperty('--ion-safe-area-top', '0px');
665
- if (isTablet) {
666
- // On tablets, card modals are inset from all edges
667
- this.zeroAllSafeAreas();
668
- }
669
- else {
670
- // On phones, card modals still extend to the bottom edge
671
- style.setProperty('--ion-safe-area-left', '0px');
672
- style.setProperty('--ion-safe-area-right', '0px');
673
- this.applyFullscreenSafeArea();
674
- }
675
- return;
676
- }
677
- // Check if modal is fullscreen via CSS custom properties
678
- // This applies to both phone and tablet sizes - custom modals may have
679
- // non-fullscreen dimensions even on phones (e.g., --height: 70%)
680
- const computedStyle = getComputedStyle(this.el);
681
- const width = computedStyle.getPropertyValue('--width').trim();
682
- const height = computedStyle.getPropertyValue('--height').trim();
683
- const isFullscreen = width === '100%' && height === '100%';
684
- if (isFullscreen) {
685
- this.applyFullscreenSafeArea();
686
- }
687
- else if (isTablet) {
688
- // Centered dialog on tablet doesn't touch edges
689
- this.zeroAllSafeAreas();
690
- }
691
- else {
692
- // Non-fullscreen modal on phone - use coordinate-based detection
693
- // to determine which edges it touches (e.g., bottom-aligned custom modals)
694
- }
695
- }
696
- /**
697
- * Applies safe-area handling for fullscreen modals.
698
- * Adds wrapper padding when no footer is present to prevent
699
- * content from overlapping system navigation areas.
700
- */
701
- applyFullscreenSafeArea() {
702
- this.skipSafeAreaCoordinateDetection = true;
703
- this.updateFooterPadding();
704
- // Watch for dynamic footer additions/removals (e.g., async data loading)
705
- // Use subtree:true to support wrapped footers in framework components
706
- // (e.g., <my-footer><ion-footer>...</ion-footer></my-footer>)
707
- if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
708
- this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
709
- this.footerObserver.observe(this.el, { childList: true, subtree: true });
710
- }
711
- }
712
- /**
713
- * Updates wrapper and shadow padding based on footer presence.
714
- * Called initially and when footer is dynamically added/removed.
715
- * Both elements must be styled identically to prevent visual mismatches.
716
- */
717
- updateFooterPadding() {
718
- if (!this.wrapperEl)
719
- return;
720
- const hasFooter = this.el.querySelector('ion-footer') !== null;
721
- // Apply to both wrapper and shadow to keep them in sync
722
- const elements = [this.wrapperEl, this.shadowEl].filter(Boolean);
723
- if (hasFooter) {
724
- elements.forEach((el) => {
725
- el.style.removeProperty('padding-bottom');
726
- el.style.removeProperty('box-sizing');
727
- });
728
- }
729
- else {
730
- elements.forEach((el) => {
731
- el.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
732
- el.style.setProperty('box-sizing', 'border-box');
733
- });
734
- }
735
- }
736
- /**
737
- * Sets all safe-area CSS variables to 0px for modals that
738
- * don't touch screen edges.
739
- */
740
- zeroAllSafeAreas() {
741
- const style = this.el.style;
742
- style.setProperty('--ion-safe-area-top', '0px');
743
- style.setProperty('--ion-safe-area-bottom', '0px');
744
- style.setProperty('--ion-safe-area-left', '0px');
745
- style.setProperty('--ion-safe-area-right', '0px');
746
- }
747
- /**
748
- * Resets all safe-area related state and styles.
749
- * Called during dismiss and disconnectedCallback to ensure clean state
750
- * for re-presentation of inline modals.
751
- */
752
- resetSafeAreaState() {
753
- var _a;
754
- this.skipSafeAreaCoordinateDetection = false;
755
- this.cachedSafeAreas = undefined;
756
- this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
757
- (_a = this.footerObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
758
- this.footerObserver = undefined;
759
- // Clear wrapper and shadow styles that may have been set for safe-area handling
760
- [this.wrapperEl, this.shadowEl].forEach((el) => {
761
- if (el) {
762
- el.style.removeProperty('padding-bottom');
763
- el.style.removeProperty('box-sizing');
764
- }
765
- });
766
- // Clear safe-area CSS variable overrides
767
- const style = this.el.style;
768
- style.removeProperty('--ion-safe-area-top');
769
- style.removeProperty('--ion-safe-area-bottom');
770
- style.removeProperty('--ion-safe-area-left');
771
- style.removeProperty('--ion-safe-area-right');
772
- }
773
- /**
774
- * Gets the root safe-area values from the document element.
775
- * Uses cached values during gestures to avoid getComputedStyle calls.
776
- */
777
- getSafeAreaValues() {
778
- if (!this.cachedSafeAreas) {
779
- const rootStyle = getComputedStyle(document.documentElement);
780
- this.cachedSafeAreas = {
781
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
782
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
783
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
784
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
785
- };
786
- }
787
- return this.cachedSafeAreas;
788
- }
789
- /**
790
- * Updates safe-area CSS variable overrides based on whether the modal
791
- * extends into each safe-area region. Called after animation
792
- * and during gestures to handle dynamic position changes.
793
- *
794
- * Optimized to avoid redundant DOM writes by tracking previous state.
795
- */
796
- updateSafeAreaOverrides() {
797
- if (this.skipSafeAreaCoordinateDetection) {
798
- return;
799
- }
800
- const wrapper = this.wrapperEl;
801
- if (!wrapper) {
802
- return;
803
- }
804
- const rect = wrapper.getBoundingClientRect();
805
- const safeAreas = this.getSafeAreaValues();
806
- const extendsIntoTop = rect.top < safeAreas.top;
807
- const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
808
- const extendsIntoLeft = rect.left < safeAreas.left;
809
- const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
810
- // Only update DOM when state actually changes
811
- const prev = this.prevSafeAreaState;
812
- const style = this.el.style;
813
- if (extendsIntoTop !== prev.top) {
814
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
815
- prev.top = extendsIntoTop;
816
- }
817
- if (extendsIntoBottom !== prev.bottom) {
818
- extendsIntoBottom
819
- ? style.removeProperty('--ion-safe-area-bottom')
820
- : style.setProperty('--ion-safe-area-bottom', '0px');
821
- prev.bottom = extendsIntoBottom;
822
- }
823
- if (extendsIntoLeft !== prev.left) {
824
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
825
- prev.left = extendsIntoLeft;
826
- }
827
- if (extendsIntoRight !== prev.right) {
828
- extendsIntoRight
829
- ? style.removeProperty('--ion-safe-area-right')
830
- : style.setProperty('--ion-safe-area-right', '0px');
831
- prev.right = extendsIntoRight;
832
- }
833
- }
834
628
  sheetOnDismiss() {
835
629
  /**
836
630
  * While the gesture animation is finishing
@@ -923,8 +717,6 @@ export class Modal {
923
717
  }
924
718
  this.currentBreakpoint = undefined;
925
719
  this.animation = undefined;
926
- // Reset safe-area state for potential re-presentation
927
- this.resetSafeAreaState();
928
720
  unlock();
929
721
  return dismissed;
930
722
  }
@@ -1182,20 +974,20 @@ export class Modal {
1182
974
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1183
975
  const isHandleCycle = handleBehavior === 'cycle';
1184
976
  const isSheetModalWithHandle = isSheetModal && showHandle;
1185
- return (h(Host, Object.assign({ key: '8f42c71ec9c9270b4218ca7eec57ca998871ac07', "no-router": true,
977
+ return (h(Host, Object.assign({ key: '87328006ea6c75ebc518ace300438492a567223e', "no-router": true,
1186
978
  // Allow the modal to be navigable when the handle is focusable
1187
979
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1188
980
  zIndex: `${20000 + this.overlayIndex}`,
1189
- }, 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 }), h("ion-backdrop", { key: '2f7b08b019c66cbfceacafa9a68b91f8da1f4084', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '6b0d1ae90e8483332f74371be5dcbd0a7b2661ba', class: "modal-shadow", ref: (el) => (this.shadowEl = el) }), h("div", Object.assign({ key: 'c96f671fb25e5de45231ea65ac52500547b2e262',
981
+ }, 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 }), h("ion-backdrop", { key: 'ee94ff8e09b691dd4ad4e4db1720f06bc3c5a469', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'bffd69b4635c22d9f249725bd952c1e93d5615c7', class: "modal-shadow" }), h("div", Object.assign({ key: '1d394d3c68916e464ff1fbf5242419f4a3d3cca1',
1190
982
  /*
1191
983
  role and aria-modal must be used on the
1192
984
  same element. They must also be set inside the
1193
985
  shadow DOM otherwise ion-button will not be highlighted
1194
986
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1195
987
  */
1196
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'e07419b01a588c0ba62ed9607c843cac7bd26337', class: "modal-handle",
988
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '2dcf58792018e557e0c323baad2d672bc99c0bb1', class: "modal-handle",
1197
989
  // Prevents the handle from receiving keyboard focus when it does not cycle
1198
- 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) })), h("slot", { key: '65f35079df8eb8b6cd262e3a08297866715ce7ea', onSlotchange: this.onSlotChange }))));
990
+ 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) })), h("slot", { key: '44164b1e8710c3895400ad9f44ecd99873874ad5', onSlotchange: this.onSlotChange }))));
1199
991
  }
1200
992
  static get is() { return "ion-modal"; }
1201
993
  static get encapsulation() { return "shadow"; }
@@ -135,6 +135,10 @@ ion-backdrop {
135
135
  :host {
136
136
  --width: 600px;
137
137
  --height: 500px;
138
+ --ion-safe-area-top: 0px;
139
+ --ion-safe-area-bottom: 0px;
140
+ --ion-safe-area-right: 0px;
141
+ --ion-safe-area-left: 0px;
138
142
  }
139
143
  }
140
144
  @media only screen and (min-width: 768px) and (min-height: 768px) {