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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/components/ion-refresher.js +3 -32
  2. package/components/modal.js +248 -13
  3. package/components/popover.js +152 -30
  4. package/dist/cjs/ion-modal.cjs.entry.js +248 -13
  5. package/dist/cjs/ion-popover.cjs.entry.js +152 -30
  6. package/dist/cjs/ion-refresher_2.cjs.entry.js +3 -32
  7. package/dist/collection/components/modal/modal.ios.css +0 -4
  8. package/dist/collection/components/modal/modal.js +127 -11
  9. package/dist/collection/components/modal/modal.md.css +0 -4
  10. package/dist/collection/components/modal/safe-area-utils.js +123 -0
  11. package/dist/collection/components/popover/animations/ios.enter.js +33 -8
  12. package/dist/collection/components/popover/animations/md.enter.js +26 -6
  13. package/dist/collection/components/popover/utils.js +96 -19
  14. package/dist/collection/components/refresher/refresher.js +4 -70
  15. package/dist/docs.json +4 -49
  16. package/dist/esm/ion-modal.entry.js +248 -13
  17. package/dist/esm/ion-popover.entry.js +152 -30
  18. package/dist/esm/ion-refresher_2.entry.js +3 -32
  19. package/dist/html.html-data.json +1 -1
  20. package/dist/ionic/ionic.esm.js +1 -1
  21. package/dist/ionic/p-36d78f36.entry.js +4 -0
  22. package/dist/ionic/p-639dd543.entry.js +4 -0
  23. package/dist/ionic/p-cc3446d5.entry.js +4 -0
  24. package/dist/types/components/modal/modal.d.ts +23 -0
  25. package/dist/types/components/modal/safe-area-utils.d.ts +55 -0
  26. package/dist/types/components/popover/utils.d.ts +23 -1
  27. package/dist/types/components/refresher/refresher-interface.d.ts +0 -7
  28. package/dist/types/components/refresher/refresher.d.ts +3 -16
  29. package/dist/types/components.d.ts +5 -16
  30. package/dist/types/interface.d.ts +1 -1
  31. package/hydrate/index.js +403 -75
  32. package/hydrate/index.mjs +403 -75
  33. package/package.json +1 -1
  34. package/dist/ionic/p-90f2d3ff.entry.js +0 -4
  35. package/dist/ionic/p-91840a80.entry.js +0 -4
  36. package/dist/ionic/p-f9061316.entry.js +0 -4
@@ -0,0 +1,123 @@
1
+ /*!
2
+ * (C) Ionic http://ionicframework.com - MIT License
3
+ */
4
+ import { win } from "../../utils/browser/index";
5
+ /**
6
+ * These thresholds match the SCSS media query breakpoints in modal.vars.scss
7
+ * that trigger the centered dialog layout (non-fullscreen modal).
8
+ *
9
+ * SCSS defines two height breakpoints: $modal-inset-min-height-small (600px)
10
+ * and $modal-inset-min-height-large (768px). We use the smaller one because
11
+ * that's the threshold where the modal transitions from fullscreen to centered
12
+ * dialog — the larger breakpoint only increases the dialog's height.
13
+ */
14
+ const MODAL_INSET_MIN_WIDTH = 768;
15
+ const MODAL_INSET_MIN_HEIGHT = 600;
16
+ const EDGE_THRESHOLD = 5;
17
+ /**
18
+ * Determines if the current viewport meets the CSS media query conditions
19
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
20
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
21
+ */
22
+ const isCenteredDialogViewport = () => {
23
+ if (!win)
24
+ return false;
25
+ return win.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
26
+ .matches;
27
+ };
28
+ /**
29
+ * Returns the initial safe-area configuration based on modal type.
30
+ * This is called before animation starts and uses configuration-based prediction.
31
+ *
32
+ * @param context - Modal context information
33
+ * @returns SafeAreaConfig with initial safe-area values
34
+ */
35
+ export const getInitialSafeAreaConfig = (context) => {
36
+ const { isSheetModal, isCardModal } = context;
37
+ // Sheet modals use bottom safe-area, and top safe-area only when fully expanded
38
+ if (isSheetModal) {
39
+ return {
40
+ top: context.currentBreakpoint === 1 ? 'inherit' : '0px',
41
+ bottom: 'inherit',
42
+ left: '0px',
43
+ right: '0px',
44
+ };
45
+ }
46
+ // Card modals need safe-area for height calculation.
47
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
48
+ if (isCardModal) {
49
+ return {
50
+ top: 'inherit',
51
+ bottom: 'inherit',
52
+ left: '0px',
53
+ right: '0px',
54
+ };
55
+ }
56
+ // On viewports that meet the centered dialog media query breakpoints,
57
+ // regular modals render as centered dialogs (not fullscreen), so they
58
+ // don't touch any screen edges and don't need safe-area insets.
59
+ if (isCenteredDialogViewport()) {
60
+ return {
61
+ top: '0px',
62
+ bottom: '0px',
63
+ left: '0px',
64
+ right: '0px',
65
+ };
66
+ }
67
+ // Fullscreen modals on phone - inherit all safe areas
68
+ return {
69
+ top: 'inherit',
70
+ bottom: 'inherit',
71
+ left: 'inherit',
72
+ right: 'inherit',
73
+ };
74
+ };
75
+ /**
76
+ * Returns safe-area configuration based on actual modal position.
77
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
78
+ *
79
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
80
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
81
+ * prediction instead. Regular modals use coordinate detection which works reliably
82
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
83
+ * once a reliable detection mechanism is available.
84
+ *
85
+ * @param wrapperEl - The modal wrapper element to measure
86
+ * @returns SafeAreaConfig based on position
87
+ */
88
+ export const getPositionBasedSafeAreaConfig = (wrapperEl) => {
89
+ var _a, _b;
90
+ const rect = wrapperEl.getBoundingClientRect();
91
+ const vh = (_a = win === null || win === void 0 ? void 0 : win.innerHeight) !== null && _a !== void 0 ? _a : 0;
92
+ const vw = (_b = win === null || win === void 0 ? void 0 : win.innerWidth) !== null && _b !== void 0 ? _b : 0;
93
+ // Only apply safe-area to sides where modal overlaps with screen edge
94
+ return {
95
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
96
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
97
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
98
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
99
+ };
100
+ };
101
+ /**
102
+ * Applies safe-area CSS custom property overrides to the modal host element.
103
+ *
104
+ * @param hostEl - The modal host element (ion-modal)
105
+ * @param config - Safe-area configuration to apply
106
+ */
107
+ export const applySafeAreaOverrides = (hostEl, config) => {
108
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
109
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
110
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
111
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
112
+ };
113
+ /**
114
+ * Clears safe-area CSS custom property overrides from the modal host element.
115
+ *
116
+ * @param hostEl - The modal host element (ion-modal)
117
+ */
118
+ export const clearSafeAreaOverrides = (hostEl) => {
119
+ hostEl.style.removeProperty('--ion-safe-area-top');
120
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
121
+ hostEl.style.removeProperty('--ion-safe-area-left');
122
+ hostEl.style.removeProperty('--ion-safe-area-right');
123
+ };
@@ -3,8 +3,17 @@
3
3
  */
4
4
  import { createAnimation } from "../../../utils/animation/animation";
5
5
  import { getElementRoot } from "../../../utils/helpers";
6
- import { calculateWindowAdjustment, getArrowDimensions, getPopoverDimensions, getPopoverPosition, shouldShowArrow, } from "../utils";
6
+ import { calculateWindowAdjustment, getArrowDimensions, getPopoverDimensions, getPopoverPosition, getSafeAreaInsets, shouldShowArrow, } from "../utils";
7
7
  const POPOVER_IOS_BODY_PADDING = 5;
8
+ /**
9
+ * Minimum edge margin for iOS popovers ensures visual spacing from screen
10
+ * edges on devices without safe areas (e.g., older iPhones without notches).
11
+ * Previously this was a hardcoded `safeAreaMargin = 25` that served dual
12
+ * purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
13
+ * insets are read dynamically, this floor preserves the visual spacing when
14
+ * safe-area values are 0.
15
+ */
16
+ const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
8
17
  /**
9
18
  * iOS Popover Enter Animation
10
19
  */
@@ -30,8 +39,16 @@ export const iosEnterAnimation = (baseEl, opts) => {
30
39
  };
31
40
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
32
41
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
33
- const margin = size === 'cover' ? 0 : 25;
34
- 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);
42
+ const rawSafeArea = getSafeAreaInsets(doc);
43
+ const safeArea = size === 'cover'
44
+ ? { top: 0, bottom: 0, left: 0, right: 0 }
45
+ : {
46
+ top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
47
+ bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
48
+ left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
49
+ right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
50
+ };
51
+ const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, arrowTop, arrowLeft, addPopoverBottomClass, hideArrow, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
35
52
  const baseAnimation = createAnimation();
36
53
  const backdropAnimation = createAnimation();
37
54
  const contentAnimation = createAnimation();
@@ -62,10 +79,14 @@ export const iosEnterAnimation = (baseEl, opts) => {
62
79
  baseEl.classList.add('popover-bottom');
63
80
  }
64
81
  if (bottom !== undefined) {
65
- contentEl.style.setProperty('bottom', `${bottom}px`);
82
+ let bottomValue = `${bottom}px`;
83
+ if (checkSafeAreaBottom) {
84
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
85
+ }
86
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
66
87
  }
67
- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
68
- const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
88
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
89
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
69
90
  let leftValue = `${left}px`;
70
91
  if (checkSafeAreaLeft) {
71
92
  leftValue = `${left}px${safeAreaLeft}`;
@@ -73,12 +94,16 @@ export const iosEnterAnimation = (baseEl, opts) => {
73
94
  if (checkSafeAreaRight) {
74
95
  leftValue = `${left}px${safeAreaRight}`;
75
96
  }
76
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
97
+ let topValue = `${top}px`;
98
+ if (checkSafeAreaTop) {
99
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
100
+ }
101
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
77
102
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
78
103
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
79
104
  if (arrowEl !== null) {
80
105
  const didAdjustBounds = results.top !== top || results.left !== left;
81
- const showArrow = shouldShowArrow(side, didAdjustBounds, ev, trigger);
106
+ const showArrow = !hideArrow && shouldShowArrow(side, didAdjustBounds, ev, trigger);
82
107
  if (showArrow) {
83
108
  arrowEl.style.setProperty('top', `calc(${arrowTop}px + var(--offset-y, 0))`);
84
109
  arrowEl.style.setProperty('left', `calc(${arrowLeft}px + var(--offset-x, 0))`);
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { createAnimation } from "../../../utils/animation/animation";
5
5
  import { getElementRoot } from "../../../utils/helpers";
6
- import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition } from "../utils";
6
+ import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition, getSafeAreaInsets } from "../utils";
7
7
  const POPOVER_MD_BODY_PADDING = 12;
8
8
  /**
9
9
  * Md Popover Enter Animation
@@ -28,7 +28,23 @@ export const mdEnterAnimation = (baseEl, opts) => {
28
28
  };
29
29
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
30
30
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
31
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
31
+ // MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
32
+ // This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
33
+ const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
34
+ 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);
35
+ const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
36
+ const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
37
+ let leftValue = `${left}px`;
38
+ if (checkSafeAreaLeft) {
39
+ leftValue = `${left}px${safeAreaLeftCalc}`;
40
+ }
41
+ if (checkSafeAreaRight) {
42
+ leftValue = `${left}px${safeAreaRightCalc}`;
43
+ }
44
+ let topValue = `${top}px`;
45
+ if (checkSafeAreaTop) {
46
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
47
+ }
32
48
  const baseAnimation = createAnimation();
33
49
  const backdropAnimation = createAnimation();
34
50
  const wrapperAnimation = createAnimation();
@@ -45,13 +61,17 @@ export const mdEnterAnimation = (baseEl, opts) => {
45
61
  contentAnimation
46
62
  .addElement(contentEl)
47
63
  .beforeStyles({
48
- top: `calc(${top}px + var(--offset-y, 0px))`,
49
- left: `calc(${left}px + var(--offset-x, 0px))`,
64
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
65
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
50
66
  'transform-origin': `${originY} ${originX}`,
51
67
  })
52
68
  .beforeAddWrite(() => {
53
69
  if (bottom !== undefined) {
54
- contentEl.style.setProperty('bottom', `${bottom}px`);
70
+ let bottomValue = `${bottom}px`;
71
+ if (checkSafeAreaBottom) {
72
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
73
+ }
74
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
55
75
  }
56
76
  })
57
77
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -63,7 +83,7 @@ export const mdEnterAnimation = (baseEl, opts) => {
63
83
  if (size === 'cover') {
64
84
  baseEl.style.setProperty('--width', `${contentWidth}px`);
65
85
  }
66
- if (originY === 'bottom') {
86
+ if (addPopoverBottomClass) {
67
87
  baseEl.classList.add('popover-bottom');
68
88
  }
69
89
  })
@@ -2,6 +2,58 @@
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
4
  import { getElementRoot, raf } from "../../utils/helpers";
5
+ /**
6
+ * Shared per-frame cache for safe-area insets. Avoids creating a temporary
7
+ * DOM element and forcing a synchronous reflow on every call within the same
8
+ * frame (e.g., multiple popovers presenting simultaneously). Invalidated
9
+ * after the next animation frame so values stay fresh across orientation
10
+ * changes and viewport resizes.
11
+ */
12
+ let cachedInsets = null;
13
+ let cacheInvalidationScheduled = false;
14
+ /**
15
+ * Resolves the current --ion-safe-area-* CSS custom property values
16
+ * to actual pixel numbers. A temporary element is needed because
17
+ * getComputedStyle on :root returns the declared value of custom
18
+ * properties (e.g. "env(safe-area-inset-top)") rather than a
19
+ * resolved number. By assigning them to standard padding properties,
20
+ * the browser resolves the value.
21
+ *
22
+ * Results are cached for the current frame to avoid repeated reflows.
23
+ */
24
+ export const getSafeAreaInsets = (doc) => {
25
+ if (cachedInsets !== null) {
26
+ return cachedInsets;
27
+ }
28
+ if (doc.body === null) {
29
+ return { top: 0, bottom: 0, left: 0, right: 0 };
30
+ }
31
+ const el = doc.createElement('div');
32
+ el.style.cssText =
33
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
34
+ 'padding-top:var(--ion-safe-area-top,0px);' +
35
+ 'padding-bottom:var(--ion-safe-area-bottom,0px);' +
36
+ 'padding-left:var(--ion-safe-area-left,0px);' +
37
+ 'padding-right:var(--ion-safe-area-right,0px);';
38
+ doc.body.appendChild(el);
39
+ const style = getComputedStyle(el);
40
+ const insets = {
41
+ top: parseFloat(style.paddingTop) || 0,
42
+ bottom: parseFloat(style.paddingBottom) || 0,
43
+ left: parseFloat(style.paddingLeft) || 0,
44
+ right: parseFloat(style.paddingRight) || 0,
45
+ };
46
+ el.remove();
47
+ cachedInsets = insets;
48
+ if (!cacheInvalidationScheduled) {
49
+ cacheInvalidationScheduled = true;
50
+ raf(() => {
51
+ cachedInsets = null;
52
+ cacheInvalidationScheduled = false;
53
+ });
54
+ }
55
+ return insets;
56
+ };
5
57
  /**
6
58
  * Returns the dimensions of the popover
7
59
  * arrow on `ios` mode. If arrow is disabled
@@ -639,8 +691,10 @@ const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, con
639
691
  * Adjusts popover positioning coordinates
640
692
  * such that popover does not appear offscreen
641
693
  * or overlapping safe area bounds.
694
+ *
695
+ * @internal - This is not part of the public API.
642
696
  */
643
- export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeAreaMargin, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
697
+ export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
644
698
  let arrowTop = coordArrowTop;
645
699
  const arrowLeft = coordArrowLeft;
646
700
  let left = coordLeft;
@@ -650,16 +704,19 @@ export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding
650
704
  let originY = contentOriginY;
651
705
  let checkSafeAreaLeft = false;
652
706
  let checkSafeAreaRight = false;
707
+ let checkSafeAreaTop = false;
708
+ let checkSafeAreaBottom = false;
653
709
  const triggerTop = triggerCoordinates
654
710
  ? triggerCoordinates.top + triggerCoordinates.height
655
711
  : bodyHeight / 2 - contentHeight / 2;
656
712
  const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
657
713
  let addPopoverBottomClass = false;
714
+ const hideArrow = false;
658
715
  /**
659
716
  * Adjust popover so it does not
660
717
  * go off the left of the screen.
661
718
  */
662
- if (left < bodyPadding + safeAreaMargin) {
719
+ if (left < bodyPadding + safeArea.left) {
663
720
  left = bodyPadding;
664
721
  checkSafeAreaLeft = true;
665
722
  originX = 'left';
@@ -668,41 +725,58 @@ export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding
668
725
  * go off the right of the screen.
669
726
  */
670
727
  }
671
- else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) {
728
+ else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
672
729
  checkSafeAreaRight = true;
673
730
  left = bodyWidth - contentWidth - bodyPadding;
674
731
  originX = 'right';
675
732
  }
676
733
  /**
677
734
  * Adjust popover so it does not
678
- * go off the top of the screen.
735
+ * go off the bottom of the screen
736
+ * or overlap the bottom safe area.
679
737
  * If popover is on the left or the right of
680
738
  * the trigger, then we should not adjust top
681
739
  * margins.
682
740
  */
683
- if (triggerTop + triggerHeight + contentHeight > bodyHeight && (side === 'top' || side === 'bottom')) {
684
- if (triggerTop - contentHeight > 0) {
741
+ if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
742
+ (side === 'top' || side === 'bottom')) {
743
+ /**
744
+ * Calculate where the popover top would be if flipped
745
+ * above the trigger. Check whether that position clears
746
+ * the top safe area with room for bodyPadding.
747
+ */
748
+ const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
749
+ if (idealFlipTop >= safeArea.top + bodyPadding) {
685
750
  /**
686
- * While we strive to align the popover with the trigger
687
- * on smaller screens this is not always possible. As a result,
688
- * we adjust the popover up so that it does not hang
689
- * off the bottom of the screen. However, we do not want to move
690
- * the popover up so much that it goes off the top of the screen.
691
- *
692
- * We chose 12 here so that the popover position looks a bit nicer as
693
- * it is not right up against the edge of the screen.
751
+ * Popover fits above the trigger without overlapping
752
+ * the top safe area. Use the ideal position directly —
753
+ * no safe-area CSS vars needed since it clears both edges.
694
754
  */
695
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
755
+ top = idealFlipTop;
696
756
  arrowTop = top + contentHeight;
697
757
  originY = 'bottom';
698
758
  addPopoverBottomClass = true;
699
- /**
700
- * If not enough room for popover to appear
701
- * above trigger, then cut it off.
702
- */
703
759
  }
704
760
  else {
761
+ /**
762
+ * Can't flip above the trigger. Constrain the bottom
763
+ * edge while keeping the top near the trigger. This
764
+ * creates a scrollable area anchored to the trigger.
765
+ */
705
766
  bottom = bodyPadding;
767
+ checkSafeAreaBottom = true;
768
+ /**
769
+ * When the trigger is near the bottom of the screen,
770
+ * the calculated top may be at or past the bottom
771
+ * constraint, leaving zero visible height. In that
772
+ * case, pull top up to the top safe area so the
773
+ * popover fills the available space between safe areas.
774
+ */
775
+ const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
776
+ if (top >= bottomEdge) {
777
+ top = safeArea.top + bodyPadding;
778
+ checkSafeAreaTop = true;
779
+ }
706
780
  }
707
781
  }
708
782
  return {
@@ -713,9 +787,12 @@ export const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding
713
787
  originY,
714
788
  checkSafeAreaLeft,
715
789
  checkSafeAreaRight,
790
+ checkSafeAreaTop,
791
+ checkSafeAreaBottom,
716
792
  arrowTop,
717
793
  arrowLeft,
718
794
  addPopoverBottomClass,
795
+ hideArrow,
719
796
  };
720
797
  };
721
798
  export const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
@@ -68,8 +68,8 @@ export class Refresher {
68
68
  * than `1`. The default value is `1` which is equal to the speed of the cursor.
69
69
  * If a negative value is passed in, the factor will be `1` instead.
70
70
  *
71
- * For example, If the value passed is `1.2` and the content is dragged by
72
- * `10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels
71
+ * For example: If the value passed is `1.2` and the content is dragged by
72
+ * `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels
73
73
  * (an increase of 20 percent). If the value passed is `0.8`, the dragged amount
74
74
  * will be `8` pixels, less than the amount the cursor has moved.
75
75
  *
@@ -119,9 +119,6 @@ export class Refresher {
119
119
  this.animations = [];
120
120
  this.progress = 0;
121
121
  this.state = 1 /* RefresherState.Inactive */;
122
- this.ionPullEnd.emit({
123
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
124
- });
125
122
  }
126
123
  async setupiOSNativeRefresher(pullingSpinner, refreshingSpinner) {
127
124
  this.elementToTransform = this.scrollEl;
@@ -154,7 +151,6 @@ export class Refresher {
154
151
  if (!this.didStart) {
155
152
  this.didStart = true;
156
153
  this.ionStart.emit();
157
- this.ionPullStart.emit();
158
154
  }
159
155
  // emit "pulling" on every move
160
156
  if (this.pointerDown) {
@@ -230,7 +226,6 @@ export class Refresher {
230
226
  this.lastVelocityY = ev.velocityY;
231
227
  },
232
228
  onEnd: () => {
233
- const hadStarted = this.didStart;
234
229
  this.pointerDown = false;
235
230
  this.didStart = false;
236
231
  if (this.needsCompletion) {
@@ -240,13 +235,6 @@ export class Refresher {
240
235
  else if (this.didRefresh) {
241
236
  readTask(() => translateElement(this.elementToTransform, `${this.el.clientHeight}px`));
242
237
  }
243
- else if (hadStarted) {
244
- /**
245
- * User started pulling but released before reaching the refresh threshold.
246
- * Emit ionPullEnd to complete the event pair.
247
- */
248
- this.ionPullEnd.emit({ reason: 'cancel' });
249
- }
250
238
  },
251
239
  });
252
240
  this.disabledChanged();
@@ -293,7 +281,6 @@ export class Refresher {
293
281
  ev.data.animation = animation;
294
282
  animation.progressStart(false, 0);
295
283
  this.ionStart.emit();
296
- this.ionPullStart.emit();
297
284
  this.animations.push(animation);
298
285
  return;
299
286
  }
@@ -316,7 +303,6 @@ export class Refresher {
316
303
  this.animations = [];
317
304
  this.gesture.enable(true);
318
305
  this.state = 1 /* RefresherState.Inactive */;
319
- this.ionPullEnd.emit({ reason: 'cancel' });
320
306
  });
321
307
  return;
322
308
  }
@@ -561,7 +547,6 @@ export class Refresher {
561
547
  if (!this.didStart) {
562
548
  this.didStart = true;
563
549
  this.ionStart.emit();
564
- this.ionPullStart.emit();
565
550
  }
566
551
  // emit "pulling" on every move
567
552
  this.ionPull.emit();
@@ -605,15 +590,6 @@ export class Refresher {
605
590
  * available right away.
606
591
  */
607
592
  this.restoreOverflowStyle();
608
- /**
609
- * If ionPullStart was emitted, we need to emit ionPullEnd
610
- * even though the gesture was aborted before reaching the
611
- * pulling threshold.
612
- */
613
- if (this.didStart) {
614
- this.didStart = false;
615
- this.ionPullEnd.emit({ reason: 'cancel' });
616
- }
617
593
  }
618
594
  }
619
595
  beginRefresh() {
@@ -660,9 +636,6 @@ export class Refresher {
660
636
  if (this.contentFullscreen && this.backgroundContentEl) {
661
637
  (_a = this.backgroundContentEl) === null || _a === void 0 ? void 0 : _a.style.removeProperty('--offset-top');
662
638
  }
663
- this.ionPullEnd.emit({
664
- reason: state === 32 /* RefresherState.Completing */ ? 'complete' : 'cancel',
665
- });
666
639
  }, 600);
667
640
  // reset the styles on the scroll element
668
641
  // set that the refresh is actively cancelling/completing
@@ -716,7 +689,7 @@ export class Refresher {
716
689
  }
717
690
  render() {
718
691
  const mode = getIonMode(this);
719
- return (h(Host, { key: 'bed0c2f44c32ecac24b7d8326ac7c7ef1d12c44f', slot: "fixed", class: {
692
+ return (h(Host, { key: '2d1bd880877b698604542ab2d602d38b9504d975', slot: "fixed", class: {
720
693
  [mode]: true,
721
694
  // Used internally for styling
722
695
  [`refresher-${mode}`]: true,
@@ -836,7 +809,7 @@ export class Refresher {
836
809
  "optional": false,
837
810
  "docs": {
838
811
  "tags": [],
839
- "text": "How much to multiply the pull speed by. To slow the pull animation down,\npass a number less than `1`. To speed up the pull, pass a number greater\nthan `1`. The default value is `1` which is equal to the speed of the cursor.\nIf a negative value is passed in, the factor will be `1` instead.\n\nFor example, If the value passed is `1.2` and the content is dragged by\n`10` pixels, instead of `10` pixels, the content will be pulled by `12` pixels\n(an increase of 20 percent). If the value passed is `0.8`, the dragged amount\nwill be `8` pixels, less than the amount the cursor has moved.\n\nDoes not apply when the refresher content uses a spinner,\nenabling the native refresher."
812
+ "text": "How much to multiply the pull speed by. To slow the pull animation down,\npass a number less than `1`. To speed up the pull, pass a number greater\nthan `1`. The default value is `1` which is equal to the speed of the cursor.\nIf a negative value is passed in, the factor will be `1` instead.\n\nFor example: If the value passed is `1.2` and the content is dragged by\n`10` pixels, instead of `10` pixels the content will be pulled by `12` pixels\n(an increase of 20 percent). If the value passed is `0.8`, the dragged amount\nwill be `8` pixels, less than the amount the cursor has moved.\n\nDoes not apply when the refresher content uses a spinner,\nenabling the native refresher."
840
813
  },
841
814
  "getter": false,
842
815
  "setter": false,
@@ -915,24 +888,6 @@ export class Refresher {
915
888
  "bubbles": true,
916
889
  "cancelable": true,
917
890
  "composed": true,
918
- "docs": {
919
- "tags": [{
920
- "name": "deprecated",
921
- "text": "Use `ionPullStart` instead."
922
- }],
923
- "text": "Emitted when the user begins to start pulling down.\nTODO(FW-7044): Remove this in a major release"
924
- },
925
- "complexType": {
926
- "original": "void",
927
- "resolved": "void",
928
- "references": {}
929
- }
930
- }, {
931
- "method": "ionPullStart",
932
- "name": "ionPullStart",
933
- "bubbles": true,
934
- "cancelable": true,
935
- "composed": true,
936
891
  "docs": {
937
892
  "tags": [],
938
893
  "text": "Emitted when the user begins to start pulling down."
@@ -942,27 +897,6 @@ export class Refresher {
942
897
  "resolved": "void",
943
898
  "references": {}
944
899
  }
945
- }, {
946
- "method": "ionPullEnd",
947
- "name": "ionPullEnd",
948
- "bubbles": true,
949
- "cancelable": true,
950
- "composed": true,
951
- "docs": {
952
- "tags": [],
953
- "text": "Emitted when the refresher has returned to the inactive state\nafter a pull gesture. This fires whether the refresh completed\nsuccessfully or was canceled."
954
- },
955
- "complexType": {
956
- "original": "RefresherPullEndEventDetail",
957
- "resolved": "RefresherPullEndEventDetail",
958
- "references": {
959
- "RefresherPullEndEventDetail": {
960
- "location": "import",
961
- "path": "./refresher-interface",
962
- "id": "src/components/refresher/refresher-interface.ts::RefresherPullEndEventDetail"
963
- }
964
- }
965
- }
966
900
  }];
967
901
  }
968
902
  static get methods() {