@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
@@ -13,6 +13,58 @@ import { e as deepReady, w as waitForMount } from './index2.js';
13
13
  import { c as createAnimation } from './animation.js';
14
14
  import { d as defineCustomElement$1 } from './backdrop.js';
15
15
 
16
+ /**
17
+ * Shared per-frame cache for safe-area insets. Avoids creating a temporary
18
+ * DOM element and forcing a synchronous reflow on every call within the same
19
+ * frame (e.g., multiple popovers presenting simultaneously). Invalidated
20
+ * after the next animation frame so values stay fresh across orientation
21
+ * changes and viewport resizes.
22
+ */
23
+ let cachedInsets = null;
24
+ let cacheInvalidationScheduled = false;
25
+ /**
26
+ * Resolves the current --ion-safe-area-* CSS custom property values
27
+ * to actual pixel numbers. A temporary element is needed because
28
+ * getComputedStyle on :root returns the declared value of custom
29
+ * properties (e.g. "env(safe-area-inset-top)") rather than a
30
+ * resolved number. By assigning them to standard padding properties,
31
+ * the browser resolves the value.
32
+ *
33
+ * Results are cached for the current frame to avoid repeated reflows.
34
+ */
35
+ const getSafeAreaInsets = (doc) => {
36
+ if (cachedInsets !== null) {
37
+ return cachedInsets;
38
+ }
39
+ if (doc.body === null) {
40
+ return { top: 0, bottom: 0, left: 0, right: 0 };
41
+ }
42
+ const el = doc.createElement('div');
43
+ el.style.cssText =
44
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
45
+ 'padding-top:var(--ion-safe-area-top,0px);' +
46
+ 'padding-bottom:var(--ion-safe-area-bottom,0px);' +
47
+ 'padding-left:var(--ion-safe-area-left,0px);' +
48
+ 'padding-right:var(--ion-safe-area-right,0px);';
49
+ doc.body.appendChild(el);
50
+ const style = getComputedStyle(el);
51
+ const insets = {
52
+ top: parseFloat(style.paddingTop) || 0,
53
+ bottom: parseFloat(style.paddingBottom) || 0,
54
+ left: parseFloat(style.paddingLeft) || 0,
55
+ right: parseFloat(style.paddingRight) || 0,
56
+ };
57
+ el.remove();
58
+ cachedInsets = insets;
59
+ if (!cacheInvalidationScheduled) {
60
+ cacheInvalidationScheduled = true;
61
+ raf(() => {
62
+ cachedInsets = null;
63
+ cacheInvalidationScheduled = false;
64
+ });
65
+ }
66
+ return insets;
67
+ };
16
68
  /**
17
69
  * Returns the dimensions of the popover
18
70
  * arrow on `ios` mode. If arrow is disabled
@@ -648,8 +700,10 @@ const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, con
648
700
  * Adjusts popover positioning coordinates
649
701
  * such that popover does not appear offscreen
650
702
  * or overlapping safe area bounds.
703
+ *
704
+ * @internal - This is not part of the public API.
651
705
  */
652
- const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeAreaMargin, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
706
+ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
653
707
  let arrowTop = coordArrowTop;
654
708
  const arrowLeft = coordArrowLeft;
655
709
  let left = coordLeft;
@@ -659,16 +713,19 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
659
713
  let originY = contentOriginY;
660
714
  let checkSafeAreaLeft = false;
661
715
  let checkSafeAreaRight = false;
716
+ let checkSafeAreaTop = false;
717
+ let checkSafeAreaBottom = false;
662
718
  const triggerTop = triggerCoordinates
663
719
  ? triggerCoordinates.top + triggerCoordinates.height
664
720
  : bodyHeight / 2 - contentHeight / 2;
665
721
  const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
666
722
  let addPopoverBottomClass = false;
723
+ const hideArrow = false;
667
724
  /**
668
725
  * Adjust popover so it does not
669
726
  * go off the left of the screen.
670
727
  */
671
- if (left < bodyPadding + safeAreaMargin) {
728
+ if (left < bodyPadding + safeArea.left) {
672
729
  left = bodyPadding;
673
730
  checkSafeAreaLeft = true;
674
731
  originX = 'left';
@@ -677,41 +734,58 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
677
734
  * go off the right of the screen.
678
735
  */
679
736
  }
680
- else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) {
737
+ else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
681
738
  checkSafeAreaRight = true;
682
739
  left = bodyWidth - contentWidth - bodyPadding;
683
740
  originX = 'right';
684
741
  }
685
742
  /**
686
743
  * Adjust popover so it does not
687
- * go off the top of the screen.
744
+ * go off the bottom of the screen
745
+ * or overlap the bottom safe area.
688
746
  * If popover is on the left or the right of
689
747
  * the trigger, then we should not adjust top
690
748
  * margins.
691
749
  */
692
- if (triggerTop + triggerHeight + contentHeight > bodyHeight && (side === 'top' || side === 'bottom')) {
693
- if (triggerTop - contentHeight > 0) {
750
+ if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
751
+ (side === 'top' || side === 'bottom')) {
752
+ /**
753
+ * Calculate where the popover top would be if flipped
754
+ * above the trigger. Check whether that position clears
755
+ * the top safe area with room for bodyPadding.
756
+ */
757
+ const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
758
+ if (idealFlipTop >= safeArea.top + bodyPadding) {
694
759
  /**
695
- * While we strive to align the popover with the trigger
696
- * on smaller screens this is not always possible. As a result,
697
- * we adjust the popover up so that it does not hang
698
- * off the bottom of the screen. However, we do not want to move
699
- * the popover up so much that it goes off the top of the screen.
700
- *
701
- * We chose 12 here so that the popover position looks a bit nicer as
702
- * it is not right up against the edge of the screen.
760
+ * Popover fits above the trigger without overlapping
761
+ * the top safe area. Use the ideal position directly —
762
+ * no safe-area CSS vars needed since it clears both edges.
703
763
  */
704
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
764
+ top = idealFlipTop;
705
765
  arrowTop = top + contentHeight;
706
766
  originY = 'bottom';
707
767
  addPopoverBottomClass = true;
708
- /**
709
- * If not enough room for popover to appear
710
- * above trigger, then cut it off.
711
- */
712
768
  }
713
769
  else {
770
+ /**
771
+ * Can't flip above the trigger. Constrain the bottom
772
+ * edge while keeping the top near the trigger. This
773
+ * creates a scrollable area anchored to the trigger.
774
+ */
714
775
  bottom = bodyPadding;
776
+ checkSafeAreaBottom = true;
777
+ /**
778
+ * When the trigger is near the bottom of the screen,
779
+ * the calculated top may be at or past the bottom
780
+ * constraint, leaving zero visible height. In that
781
+ * case, pull top up to the top safe area so the
782
+ * popover fills the available space between safe areas.
783
+ */
784
+ const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
785
+ if (top >= bottomEdge) {
786
+ top = safeArea.top + bodyPadding;
787
+ checkSafeAreaTop = true;
788
+ }
715
789
  }
716
790
  }
717
791
  return {
@@ -722,9 +796,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
722
796
  originY,
723
797
  checkSafeAreaLeft,
724
798
  checkSafeAreaRight,
799
+ checkSafeAreaTop,
800
+ checkSafeAreaBottom,
725
801
  arrowTop,
726
802
  arrowLeft,
727
803
  addPopoverBottomClass,
804
+ hideArrow,
728
805
  };
729
806
  };
730
807
  const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
@@ -754,6 +831,15 @@ const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
754
831
  };
755
832
 
756
833
  const POPOVER_IOS_BODY_PADDING = 5;
834
+ /**
835
+ * Minimum edge margin for iOS popovers ensures visual spacing from screen
836
+ * edges on devices without safe areas (e.g., older iPhones without notches).
837
+ * Previously this was a hardcoded `safeAreaMargin = 25` that served dual
838
+ * purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
839
+ * insets are read dynamically, this floor preserves the visual spacing when
840
+ * safe-area values are 0.
841
+ */
842
+ const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
757
843
  /**
758
844
  * iOS Popover Enter Animation
759
845
  */
@@ -779,8 +865,16 @@ const iosEnterAnimation = (baseEl, opts) => {
779
865
  };
780
866
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
781
867
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
782
- const margin = size === 'cover' ? 0 : 25;
783
- 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);
868
+ const rawSafeArea = getSafeAreaInsets(doc);
869
+ const safeArea = size === 'cover'
870
+ ? { top: 0, bottom: 0, left: 0, right: 0 }
871
+ : {
872
+ top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
873
+ bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
874
+ left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
875
+ right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
876
+ };
877
+ 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);
784
878
  const baseAnimation = createAnimation();
785
879
  const backdropAnimation = createAnimation();
786
880
  const contentAnimation = createAnimation();
@@ -811,10 +905,14 @@ const iosEnterAnimation = (baseEl, opts) => {
811
905
  baseEl.classList.add('popover-bottom');
812
906
  }
813
907
  if (bottom !== undefined) {
814
- contentEl.style.setProperty('bottom', `${bottom}px`);
908
+ let bottomValue = `${bottom}px`;
909
+ if (checkSafeAreaBottom) {
910
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
911
+ }
912
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
815
913
  }
816
- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
817
- const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
914
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
915
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
818
916
  let leftValue = `${left}px`;
819
917
  if (checkSafeAreaLeft) {
820
918
  leftValue = `${left}px${safeAreaLeft}`;
@@ -822,7 +920,11 @@ const iosEnterAnimation = (baseEl, opts) => {
822
920
  if (checkSafeAreaRight) {
823
921
  leftValue = `${left}px${safeAreaRight}`;
824
922
  }
825
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
923
+ let topValue = `${top}px`;
924
+ if (checkSafeAreaTop) {
925
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
926
+ }
927
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
826
928
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
827
929
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
828
930
  if (arrowEl !== null) {
@@ -898,7 +1000,23 @@ const mdEnterAnimation = (baseEl, opts) => {
898
1000
  };
899
1001
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
900
1002
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
901
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
1003
+ // MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
1004
+ // This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
1005
+ const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
1006
+ 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);
1007
+ const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
1008
+ const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
1009
+ let leftValue = `${left}px`;
1010
+ if (checkSafeAreaLeft) {
1011
+ leftValue = `${left}px${safeAreaLeftCalc}`;
1012
+ }
1013
+ if (checkSafeAreaRight) {
1014
+ leftValue = `${left}px${safeAreaRightCalc}`;
1015
+ }
1016
+ let topValue = `${top}px`;
1017
+ if (checkSafeAreaTop) {
1018
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
1019
+ }
902
1020
  const baseAnimation = createAnimation();
903
1021
  const backdropAnimation = createAnimation();
904
1022
  const wrapperAnimation = createAnimation();
@@ -915,13 +1033,17 @@ const mdEnterAnimation = (baseEl, opts) => {
915
1033
  contentAnimation
916
1034
  .addElement(contentEl)
917
1035
  .beforeStyles({
918
- top: `calc(${top}px + var(--offset-y, 0px))`,
919
- left: `calc(${left}px + var(--offset-x, 0px))`,
1036
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
1037
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
920
1038
  'transform-origin': `${originY} ${originX}`,
921
1039
  })
922
1040
  .beforeAddWrite(() => {
923
1041
  if (bottom !== undefined) {
924
- contentEl.style.setProperty('bottom', `${bottom}px`);
1042
+ let bottomValue = `${bottom}px`;
1043
+ if (checkSafeAreaBottom) {
1044
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
1045
+ }
1046
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
925
1047
  }
926
1048
  })
927
1049
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -933,7 +1055,7 @@ const mdEnterAnimation = (baseEl, opts) => {
933
1055
  if (size === 'cover') {
934
1056
  baseEl.style.setProperty('--width', `${contentWidth}px`);
935
1057
  }
936
- if (originY === 'bottom') {
1058
+ if (addPopoverBottomClass) {
937
1059
  baseEl.classList.add('popover-bottom');
938
1060
  }
939
1061
  })