@ionic/core 8.7.18-dev.11771365900.125bf49c → 8.7.18-dev.11772217280.122eed89

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 (90) hide show
  1. package/components/ion-app.js +17 -12
  2. package/components/ion-checkbox.js +1 -1
  3. package/components/ion-content.js +1 -1
  4. package/components/ion-datetime.js +7 -4
  5. package/components/ion-fab-list.js +10 -3
  6. package/components/ion-img.js +11 -2
  7. package/components/ion-label.js +1 -1
  8. package/components/ion-modal.js +1 -1
  9. package/components/ion-popover.js +1 -1
  10. package/components/ion-radio-group.js +1 -1
  11. package/components/ion-searchbar.js +17 -6
  12. package/components/ion-select-modal.js +1 -1
  13. package/components/ion-select-popover.js +1 -1
  14. package/components/ion-select.js +8 -8
  15. package/components/{p-CJrGvZ1Z.js → p-51c2oSfC.js} +3 -3
  16. package/components/{p-BnpDIASH.js → p-BANeLH_G.js} +2 -2
  17. package/components/{p-D3KEyQ8e.js → p-BRLve2EP.js} +3 -3
  18. package/components/{p-DRHwNkVo.js → p-C6dkQWq2.js} +152 -30
  19. package/components/{p-CUfY9DTD.js → p-Cdm3dKWf.js} +8 -3
  20. package/components/{p-DyFBFrV7.js → p-D--b4514.js} +4 -4
  21. package/components/{p-DPm73mCi.js → p-DGOrLIyS.js} +7 -3
  22. package/components/{p-IHgAGzvW.js → p-iyo0Toaz.js} +326 -17
  23. package/dist/cjs/ion-app_8.cjs.entry.js +22 -13
  24. package/dist/cjs/ion-checkbox.cjs.entry.js +3 -3
  25. package/dist/cjs/ion-datetime_3.cjs.entry.js +6 -3
  26. package/dist/cjs/ion-fab_3.cjs.entry.js +10 -3
  27. package/dist/cjs/ion-img.cjs.entry.js +11 -2
  28. package/dist/cjs/ion-item_8.cjs.entry.js +8 -3
  29. package/dist/cjs/ion-modal.cjs.entry.js +326 -17
  30. package/dist/cjs/ion-popover.cjs.entry.js +152 -30
  31. package/dist/cjs/ion-radio_2.cjs.entry.js +2 -2
  32. package/dist/cjs/ion-searchbar.cjs.entry.js +17 -6
  33. package/dist/collection/components/app/app.js +15 -10
  34. package/dist/collection/components/checkbox/checkbox.js +4 -4
  35. package/dist/collection/components/content/content.js +7 -3
  36. package/dist/collection/components/datetime/datetime.js +6 -3
  37. package/dist/collection/components/fab-list/fab-list.js +10 -3
  38. package/dist/collection/components/img/img.js +11 -2
  39. package/dist/collection/components/label/label.js +8 -3
  40. package/dist/collection/components/modal/modal.ios.css +6 -5
  41. package/dist/collection/components/modal/modal.js +164 -15
  42. package/dist/collection/components/modal/modal.md.css +6 -5
  43. package/dist/collection/components/modal/safe-area-utils.js +165 -0
  44. package/dist/collection/components/popover/animations/ios.enter.js +33 -8
  45. package/dist/collection/components/popover/animations/md.enter.js +26 -6
  46. package/dist/collection/components/popover/utils.js +96 -19
  47. package/dist/collection/components/radio-group/radio-group.ios.css +0 -4
  48. package/dist/collection/components/radio-group/radio-group.md.css +0 -4
  49. package/dist/collection/components/searchbar/searchbar.js +17 -6
  50. package/dist/docs.json +1 -1
  51. package/dist/esm/ion-app_8.entry.js +24 -15
  52. package/dist/esm/ion-checkbox.entry.js +4 -4
  53. package/dist/esm/ion-datetime_3.entry.js +6 -3
  54. package/dist/esm/ion-fab_3.entry.js +10 -3
  55. package/dist/esm/ion-img.entry.js +11 -2
  56. package/dist/esm/ion-item_8.entry.js +8 -3
  57. package/dist/esm/ion-modal.entry.js +326 -17
  58. package/dist/esm/ion-popover.entry.js +152 -30
  59. package/dist/esm/ion-radio_2.entry.js +2 -2
  60. package/dist/esm/ion-searchbar.entry.js +17 -6
  61. package/dist/ionic/ionic.esm.js +1 -1
  62. package/dist/ionic/{p-72af946b.entry.js → p-2ac254dc.entry.js} +1 -1
  63. package/dist/ionic/p-7cad388a.entry.js +4 -0
  64. package/dist/ionic/p-7d218555.entry.js +4 -0
  65. package/dist/ionic/p-8303e3fc.entry.js +4 -0
  66. package/dist/ionic/p-915b3e3d.entry.js +4 -0
  67. package/dist/ionic/{p-eaf3f5ff.entry.js → p-b0ebf9f8.entry.js} +1 -1
  68. package/dist/ionic/p-e4c57008.entry.js +4 -0
  69. package/dist/ionic/p-e5d6d0f0.entry.js +4 -0
  70. package/dist/ionic/p-f1dfee25.entry.js +4 -0
  71. package/dist/ionic/{p-6444c606.entry.js → p-f9f7b9db.entry.js} +1 -1
  72. package/dist/types/components/app/app.d.ts +3 -0
  73. package/dist/types/components/datetime/datetime.d.ts +1 -0
  74. package/dist/types/components/fab-list/fab-list.d.ts +2 -0
  75. package/dist/types/components/img/img.d.ts +2 -0
  76. package/dist/types/components/label/label.d.ts +2 -0
  77. package/dist/types/components/modal/modal.d.ts +35 -0
  78. package/dist/types/components/modal/safe-area-utils.d.ts +66 -0
  79. package/dist/types/components/popover/utils.d.ts +23 -1
  80. package/dist/types/components/searchbar/searchbar.d.ts +3 -0
  81. package/hydrate/index.js +556 -73
  82. package/hydrate/index.mjs +556 -73
  83. package/package.json +1 -1
  84. package/dist/ionic/p-01e27965.entry.js +0 -4
  85. package/dist/ionic/p-3ec563c1.entry.js +0 -4
  86. package/dist/ionic/p-42db6404.entry.js +0 -4
  87. package/dist/ionic/p-46d74291.entry.js +0 -4
  88. package/dist/ionic/p-bc293244.entry.js +0 -4
  89. package/dist/ionic/p-d1f54e28.entry.js +0 -4
  90. package/dist/ionic/p-e338f669.entry.js +0 -4
package/hydrate/index.mjs CHANGED
@@ -8785,6 +8785,11 @@ class App {
8785
8785
  }
8786
8786
  componentDidLoad() {
8787
8787
  }
8788
+ disconnectedCallback() {
8789
+ if (this.loadTimeout) {
8790
+ clearTimeout(this.loadTimeout);
8791
+ }
8792
+ }
8788
8793
  /**
8789
8794
  * Used to set focus on an element that uses `ion-focusable`.
8790
8795
  * Do not use this if focusing the element as a result of a keyboard
@@ -8801,9 +8806,17 @@ class App {
8801
8806
  this.focusVisible.setFocus(elements);
8802
8807
  }
8803
8808
  }
8809
+ rIC(callback) {
8810
+ if ('requestIdleCallback' in window) {
8811
+ window.requestIdleCallback(callback);
8812
+ }
8813
+ else {
8814
+ this.loadTimeout = setTimeout(callback, 32);
8815
+ }
8816
+ }
8804
8817
  render() {
8805
8818
  const mode = getIonMode$1(this);
8806
- return (hAsync(Host, { key: '9be440c65819e4fa67c2c3c6477ab40b3ad3eed3', class: {
8819
+ return (hAsync(Host, { key: '77a7459d3e42b3715a5a7e1cf468e2ebc99e54bb', class: {
8807
8820
  [mode]: true,
8808
8821
  'ion-page': true,
8809
8822
  'force-statusbar-padding': config.getBoolean('_forceStatusbarPadding'),
@@ -10237,7 +10250,7 @@ class Checkbox {
10237
10250
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
10238
10251
  // The host element must have a checkbox role to ensure proper VoiceOver
10239
10252
  // support in Safari for accessibility.
10240
- return (hAsync(Host, { key: 'ae0fbd4b21accbac132e6b85c513512ad9179394', role: "checkbox", "aria-checked": indeterminate ? 'mixed' : `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-labelledby": hasLabelContent ? this.inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, "aria-required": required ? 'true' : undefined, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, onClick: this.onClick, class: createColorClasses$1(color, {
10253
+ return (hAsync(Host, { key: 'fca2e80987d5e2e7f9138eeac3314d883ea1252b', role: "checkbox", "aria-checked": indeterminate ? 'mixed' : `${checked}`, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-labelledby": hasLabelContent ? this.inputLabelId : null, "aria-label": inheritedAttributes['aria-label'] || null, "aria-disabled": disabled ? 'true' : null, "aria-required": required ? 'true' : undefined, tabindex: disabled ? undefined : 0, onKeyDown: this.onKeyDown, onFocus: this.onFocus, onBlur: this.onBlur, onClick: this.onClick, class: createColorClasses$1(color, {
10241
10254
  [mode]: true,
10242
10255
  'in-item': hostContext('ion-item', el),
10243
10256
  'checkbox-checked': checked,
@@ -10247,10 +10260,10 @@ class Checkbox {
10247
10260
  [`checkbox-justify-${justify}`]: justify !== undefined,
10248
10261
  [`checkbox-alignment-${alignment}`]: alignment !== undefined,
10249
10262
  [`checkbox-label-placement-${labelPlacement}`]: true,
10250
- }) }, hAsync("label", { key: '7a3d7f3c27dde514f2dbf2e34f4629fad33ec3bf', class: "checkbox-wrapper", htmlFor: inputId }, hAsync("input", Object.assign({ key: '4130d77ddf034271fecccda14e101a5a809921b6', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), hAsync("div", { key: '5daa74f4e62b0947e37764762524001ee42609d9', class: {
10263
+ }) }, hAsync("label", { key: '5cf50b6abdb9a3ae1f846f18bd8d5345de0ea2e9', class: "checkbox-wrapper", htmlFor: inputId }, hAsync("input", Object.assign({ key: 'b8c3c6b01f65ff1b723c0de4b5d6f1b62940f481', type: "checkbox", checked: checked ? true : undefined, disabled: disabled, id: inputId, onChange: this.toggleChecked, required: required }, inheritedAttributes)), hAsync("div", { key: '7b29b18b97b8151f454760d73aca1543c8325e41', class: {
10251
10264
  'label-text-wrapper': true,
10252
10265
  'label-text-wrapper-hidden': !hasLabelContent,
10253
- }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, hAsync("slot", { key: '23ff66138f8c3a2f56f39113fc842d54b2f7952a' }), this.renderHintText()), hAsync("div", { key: 'ab914d9623c19fc46821d5e62db92f1192ebbe7e', class: "native-wrapper" }, hAsync("svg", { key: '66e3f4f5dcaa9756fb0e9452299954f9ed3dcb7b', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
10266
+ }, part: "label", id: this.inputLabelId, onClick: this.onDivLabelClick }, hAsync("slot", { key: 'ffbce8010274a68b8e67f9c0f19ec012944df484', onSlotchange: () => forceUpdate() }), this.renderHintText()), hAsync("div", { key: '8e21fb89c72fbd52df70fa1916367ab5a543f281', class: "native-wrapper" }, hAsync("svg", { key: 'c2519ff1a5c7f69e03ab26197ccfa8718b0951c6', class: "checkbox-icon", viewBox: "0 0 24 24", part: "container", "aria-hidden": "true" }, path)))));
10254
10267
  }
10255
10268
  getSVGPath(mode, indeterminate) {
10256
10269
  let path = indeterminate ? (hAsync("path", { d: "M6 12L18 12", part: "mark" })) : (hAsync("path", { d: "M5.9,12.5l3.8,3.8l8.8-8.8", part: "mark" }));
@@ -10627,6 +10640,10 @@ class Content {
10627
10640
  this.tabsElement = null;
10628
10641
  this.tabsLoadCallback = undefined;
10629
10642
  }
10643
+ if (this.resizeTimeout) {
10644
+ clearTimeout(this.resizeTimeout);
10645
+ this.resizeTimeout = null;
10646
+ }
10630
10647
  }
10631
10648
  /**
10632
10649
  * Rotating certain devices can update
@@ -10847,7 +10864,7 @@ class Content {
10847
10864
  const forceOverscroll = this.shouldForceOverscroll();
10848
10865
  const transitionShadow = mode === 'ios';
10849
10866
  this.resize();
10850
- return (hAsync(Host, Object.assign({ key: 'cd8781f848d8dc926fe66f43d43c49564425a507', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10867
+ return (hAsync(Host, Object.assign({ key: '287f5bb78888b0f66394a35ba2a3ad636d8f3412', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10851
10868
  [mode]: true,
10852
10869
  'content-sizing': hostContext('ion-popover', this.el),
10853
10870
  overscroll: forceOverscroll,
@@ -10855,12 +10872,12 @@ class Content {
10855
10872
  }), style: {
10856
10873
  '--offset-top': `${this.cTop}px`,
10857
10874
  '--offset-bottom': `${this.cBottom}px`,
10858
- } }, inheritedAttributes), hAsync("div", { key: '95b112d7cae30f22ef778ceffb88edb4d941c170', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: '2fdfcbc39fb66f11b6191911f2941c660f4c12e5', class: {
10875
+ } }, inheritedAttributes), hAsync("div", { key: '2e70e5e0ab4209478ea711a630058fa0e83af7f8', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: '018a83406173ec7f7725f09b4bd1c0d964874ee4', class: {
10859
10876
  'inner-scroll': true,
10860
10877
  'scroll-x': scrollX,
10861
10878
  'scroll-y': scrollY,
10862
10879
  overscroll: (scrollX || scrollY) && forceOverscroll,
10863
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '6bc77e0054ec8e21635a7f2abfe0ca46e0962e03' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10880
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '6f028eeb7c9dc7e1b82a8e549e1dd8aaf1d15fca' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10864
10881
  }
10865
10882
  get el() { return getElement(this); }
10866
10883
  static get style() { return contentCss; }
@@ -13735,6 +13752,9 @@ class Datetime {
13735
13752
  this.clearFocusVisible();
13736
13753
  this.clearFocusVisible = undefined;
13737
13754
  }
13755
+ if (this.loadTimeout) {
13756
+ clearTimeout(this.loadTimeout);
13757
+ }
13738
13758
  }
13739
13759
  initializeListeners() {
13740
13760
  this.initializeCalendarListener();
@@ -13783,7 +13803,7 @@ class Datetime {
13783
13803
  *
13784
13804
  * We schedule this after everything has had a chance to run.
13785
13805
  */
13786
- setTimeout(() => {
13806
+ this.loadTimeout = setTimeout(() => {
13787
13807
  this.ensureReadyIfVisible();
13788
13808
  }, 100);
13789
13809
  /**
@@ -14541,7 +14561,7 @@ class Datetime {
14541
14561
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
14542
14562
  const hasWheelVariant = hasDatePresentation && preferWheel;
14543
14563
  renderHiddenInput(true, el, name, formatValue(value), disabled);
14544
- return (hAsync(Host, { key: 'efdbc0922670a841bc667ceac392cdc1dedffd01', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses$1(color, {
14564
+ return (hAsync(Host, { key: 'b81c3531f4654be3f6948df5f790c0f6ab488000', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses$1(color, {
14545
14565
  [mode]: true,
14546
14566
  ['datetime-readonly']: readonly,
14547
14567
  ['datetime-disabled']: disabled,
@@ -14551,7 +14571,7 @@ class Datetime {
14551
14571
  [`datetime-size-${size}`]: true,
14552
14572
  [`datetime-prefer-wheel`]: hasWheelVariant,
14553
14573
  [`datetime-grid`]: isGridStyle,
14554
- })) }, hAsync("div", { key: '3f8bb75fcb0baff55182ef3aa1b535eacc58d81f', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
14574
+ })) }, hAsync("div", { key: 'f7f39f8e56146186210d0ab70ae95e829573e549', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
14555
14575
  }
14556
14576
  get el() { return getElement(this); }
14557
14577
  static get watchers() { return {
@@ -15206,6 +15226,7 @@ const fabListCss = ":host{margin-left:0;margin-right:0;margin-top:calc(100% + 10
15206
15226
  class FabList {
15207
15227
  constructor(hostRef) {
15208
15228
  registerInstance(this, hostRef);
15229
+ this.activateTimeouts = [];
15209
15230
  /**
15210
15231
  * If `true`, the fab list will show all fab buttons in the list.
15211
15232
  */
@@ -15216,20 +15237,26 @@ class FabList {
15216
15237
  this.side = 'bottom';
15217
15238
  }
15218
15239
  activatedChanged(activated) {
15240
+ this.activateTimeouts.forEach(clearTimeout);
15241
+ this.activateTimeouts = [];
15219
15242
  const fabs = Array.from(this.el.querySelectorAll('ion-fab-button'));
15220
15243
  // if showing the fabs add a timeout, else show immediately
15221
15244
  const timeout = activated ? 30 : 0;
15222
15245
  fabs.forEach((fab, i) => {
15223
- setTimeout(() => (fab.show = activated), i * timeout);
15246
+ this.activateTimeouts.push(setTimeout(() => (fab.show = activated), i * timeout));
15224
15247
  });
15225
15248
  }
15249
+ disconnectedCallback() {
15250
+ this.activateTimeouts.forEach(clearTimeout);
15251
+ this.activateTimeouts = [];
15252
+ }
15226
15253
  render() {
15227
15254
  const mode = getIonMode$1(this);
15228
- return (hAsync(Host, { key: '64b33366447f66c7f979cfac56307fbb1a6fac1c', class: {
15255
+ return (hAsync(Host, { key: '03a8920c29a93c6df4bab14753a78a8a30722ce8', class: {
15229
15256
  [mode]: true,
15230
15257
  'fab-list-active': this.activated,
15231
15258
  [`fab-list-side-${this.side}`]: true,
15232
- } }, hAsync("slot", { key: 'd9f474f7f20fd7e813db358fddc720534ca05bb6' })));
15259
+ } }, hAsync("slot", { key: 'd9e1541cb044e209bc2dff14080f3b938a2b84ae' })));
15233
15260
  }
15234
15261
  get el() { return getElement(this); }
15235
15262
  static get watchers() { return {
@@ -16435,7 +16462,16 @@ class Img {
16435
16462
  componentDidLoad() {
16436
16463
  this.addIO();
16437
16464
  }
16465
+ disconnectedCallback() {
16466
+ if (this.loadTimeout) {
16467
+ clearTimeout(this.loadTimeout);
16468
+ }
16469
+ }
16438
16470
  addIO() {
16471
+ if (this.loadTimeout) {
16472
+ clearTimeout(this.loadTimeout);
16473
+ this.loadTimeout = undefined;
16474
+ }
16439
16475
  if (this.src === undefined) {
16440
16476
  return;
16441
16477
  }
@@ -16459,7 +16495,7 @@ class Img {
16459
16495
  }
16460
16496
  else {
16461
16497
  // fall back to setTimeout for Safari and IE
16462
- setTimeout(() => this.load(), 200);
16498
+ this.loadTimeout = setTimeout(() => this.load(), 200);
16463
16499
  }
16464
16500
  }
16465
16501
  load() {
@@ -16476,7 +16512,7 @@ class Img {
16476
16512
  render() {
16477
16513
  const { loadSrc, alt, onLoad, loadError, inheritedAttributes } = this;
16478
16514
  const { draggable } = inheritedAttributes;
16479
- return (hAsync(Host, { key: 'da600442894427dee1974a28e545613afac69fca', class: getIonMode$1(this) }, hAsync("img", { key: '16df0c7069af86c0fa7ce5af598bc0f63b4eb71a', decoding: "async", src: loadSrc, alt: alt, onLoad: onLoad, onError: loadError, part: "image", draggable: isDraggable(draggable) })));
16515
+ return (hAsync(Host, { key: '9bfaa659b0680189fd29f3acb2fdce7f70be6060', class: getIonMode$1(this) }, hAsync("img", { key: 'd484beae0d5cbe0406958bfad40cdd6c05625340', decoding: "async", src: loadSrc, alt: alt, onLoad: onLoad, onError: loadError, part: "image", draggable: isDraggable(draggable) })));
16480
16516
  }
16481
16517
  get el() { return getElement(this); }
16482
16518
  static get watchers() { return {
@@ -19266,11 +19302,16 @@ class Label {
19266
19302
  }
19267
19303
  componentDidLoad() {
19268
19304
  if (this.noAnimate) {
19269
- setTimeout(() => {
19305
+ this.loadTimeout = setTimeout(() => {
19270
19306
  this.noAnimate = false;
19271
19307
  }, 1000);
19272
19308
  }
19273
19309
  }
19310
+ disconnectedCallback() {
19311
+ if (this.loadTimeout) {
19312
+ clearTimeout(this.loadTimeout);
19313
+ }
19314
+ }
19274
19315
  colorChanged() {
19275
19316
  this.emitColor();
19276
19317
  }
@@ -19299,13 +19340,13 @@ class Label {
19299
19340
  render() {
19300
19341
  const position = this.position;
19301
19342
  const mode = getIonMode$1(this);
19302
- return (hAsync(Host, { key: '684ddfa719e34d743489ff05a0df067829257c73', class: createColorClasses$1(this.color, {
19343
+ return (hAsync(Host, { key: 'd603670c442213c28737b0c052c094705b8c84ef', class: createColorClasses$1(this.color, {
19303
19344
  [mode]: true,
19304
19345
  'in-item-color': hostContext('ion-item.ion-color', this.el),
19305
19346
  [`label-${position}`]: position !== undefined,
19306
19347
  [`label-no-animate`]: this.noAnimate,
19307
19348
  'label-rtl': document.dir === 'rtl',
19308
- }) }, hAsync("slot", { key: '4aace3e2b36250e832c44f63bea21116d77220fb' })));
19349
+ }) }, hAsync("slot", { key: 'a8c3aed240a20ee5856c423662878481a1e6d4cf' })));
19309
19350
  }
19310
19351
  get el() { return getElement(this); }
19311
19352
  static get watchers() { return {
@@ -22804,9 +22845,170 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
22804
22845
  };
22805
22846
  };
22806
22847
 
22807
- const modalIosCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.4)}:host(.modal-card),:host(.modal-sheet){--border-radius:10px}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:10px}}.modal-wrapper{-webkit-transform:translate3d(0, 100%, 0);transform:translate3d(0, 100%, 0)}@media screen and (max-width: 767px){@supports (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - max(30px, var(--ion-safe-area-top)) - 10px)}}@supports not (width: max(0px, 1px)){:host(.modal-card){--height:calc(100% - 40px)}}:host(.modal-card) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}:host(.modal-card){--backdrop-opacity:0;--width:100%;-ms-flex-align:end;align-items:flex-end}:host(.modal-card) .modal-shadow{display:none}:host(.modal-card) ion-backdrop{pointer-events:none}}@media screen and (min-width: 768px){:host(.modal-card){--width:calc(100% - 120px);--height:calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));--max-width:720px;--max-height:1000px;--backdrop-opacity:0;--box-shadow:0px 0px 30px 10px rgba(0, 0, 0, 0.1);-webkit-transition:all 0.5s ease-in-out;transition:all 0.5s ease-in-out}:host(.modal-card) .modal-wrapper{-webkit-box-shadow:none;box-shadow:none}:host(.modal-card) .modal-shadow{-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow)}}:host(.modal-sheet) .modal-wrapper{border-start-start-radius:var(--border-radius);border-start-end-radius:var(--border-radius);border-end-end-radius:0;border-end-start-radius:0}";
22848
+ /**
22849
+ * These thresholds match the SCSS media query breakpoints in modal.vars.scss
22850
+ * that trigger the centered dialog layout (non-fullscreen modal).
22851
+ *
22852
+ * SCSS defines two height breakpoints: $modal-inset-min-height-small (600px)
22853
+ * and $modal-inset-min-height-large (768px). We use the smaller one because
22854
+ * that's the threshold where the modal transitions from fullscreen to centered
22855
+ * dialog — the larger breakpoint only increases the dialog's height.
22856
+ */
22857
+ const MODAL_INSET_MIN_WIDTH = 768;
22858
+ const MODAL_INSET_MIN_HEIGHT = 600;
22859
+ const EDGE_THRESHOLD = 5;
22860
+ /**
22861
+ * Cache for resolved root safe-area-top value, invalidated once per frame.
22862
+ */
22863
+ let cachedRootSafeAreaTop = null;
22864
+ let cacheInvalidationScheduled$1 = false;
22865
+ /**
22866
+ * Determines if the current viewport meets the CSS media query conditions
22867
+ * that cause regular modals to render as centered dialogs instead of fullscreen.
22868
+ * Matches: @media (min-width: 768px) and (min-height: 600px)
22869
+ */
22870
+ const isCenteredDialogViewport = () => {
22871
+ if (!win$1)
22872
+ return false;
22873
+ return win$1.matchMedia(`(min-width: ${MODAL_INSET_MIN_WIDTH}px) and (min-height: ${MODAL_INSET_MIN_HEIGHT}px)`)
22874
+ .matches;
22875
+ };
22876
+ /**
22877
+ * Resolves the current root --ion-safe-area-top value to pixels.
22878
+ * Uses a temporary element because getComputedStyle on :root returns
22879
+ * the declared value of custom properties (e.g. "env(safe-area-inset-top)")
22880
+ * rather than a resolved number.
22881
+ *
22882
+ * Results are cached for the current frame to avoid repeated reflows.
22883
+ */
22884
+ const getRootSafeAreaTop = () => {
22885
+ if (cachedRootSafeAreaTop !== null) {
22886
+ return cachedRootSafeAreaTop;
22887
+ }
22888
+ const doc = win$1 === null || win$1 === void 0 ? void 0 : win$1.document;
22889
+ if (!(doc === null || doc === void 0 ? void 0 : doc.body)) {
22890
+ return 0;
22891
+ }
22892
+ const el = doc.createElement('div');
22893
+ el.style.cssText =
22894
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' + 'padding-top:var(--ion-safe-area-top,0px);';
22895
+ doc.body.appendChild(el);
22896
+ const value = parseFloat(getComputedStyle(el).paddingTop) || 0;
22897
+ el.remove();
22898
+ cachedRootSafeAreaTop = value;
22899
+ if (!cacheInvalidationScheduled$1) {
22900
+ cacheInvalidationScheduled$1 = true;
22901
+ raf(() => {
22902
+ cachedRootSafeAreaTop = null;
22903
+ cacheInvalidationScheduled$1 = false;
22904
+ });
22905
+ }
22906
+ return value;
22907
+ };
22908
+ /**
22909
+ * Returns the initial safe-area configuration based on modal type.
22910
+ * This is called before animation starts and uses configuration-based prediction.
22911
+ *
22912
+ * @param context - Modal context information
22913
+ * @returns SafeAreaConfig with initial safe-area values
22914
+ */
22915
+ const getInitialSafeAreaConfig = (context) => {
22916
+ const { isSheetModal, isCardModal } = context;
22917
+ // Sheet modals always zero top safe-area. The sheet height offset from the
22918
+ // top edge is handled by --ion-modal-offset-top (set in modal.tsx) using
22919
+ // the resolved root value, so --ion-safe-area-top is never needed for
22920
+ // height calculation. Keeping it at 0px prevents header content from
22921
+ // getting double-offset padding.
22922
+ if (isSheetModal) {
22923
+ return {
22924
+ top: '0px',
22925
+ bottom: 'inherit',
22926
+ left: '0px',
22927
+ right: '0px',
22928
+ };
22929
+ }
22930
+ // Card modals need safe-area for height calculation.
22931
+ // Note: isCardModal is already gated on mode === 'ios' by the caller.
22932
+ if (isCardModal) {
22933
+ return {
22934
+ top: 'inherit',
22935
+ bottom: 'inherit',
22936
+ left: '0px',
22937
+ right: '0px',
22938
+ };
22939
+ }
22940
+ // On viewports that meet the centered dialog media query breakpoints,
22941
+ // regular modals render as centered dialogs (not fullscreen), so they
22942
+ // don't touch any screen edges and don't need safe-area insets.
22943
+ if (isCenteredDialogViewport()) {
22944
+ return {
22945
+ top: '0px',
22946
+ bottom: '0px',
22947
+ left: '0px',
22948
+ right: '0px',
22949
+ };
22950
+ }
22951
+ // Fullscreen modals on phone - inherit all safe areas
22952
+ return {
22953
+ top: 'inherit',
22954
+ bottom: 'inherit',
22955
+ left: 'inherit',
22956
+ right: 'inherit',
22957
+ };
22958
+ };
22959
+ /**
22960
+ * Returns safe-area configuration based on actual modal position.
22961
+ * Detects which edges the modal overlaps with and only applies safe-area to those edges.
22962
+ *
22963
+ * Note: On Android edge-to-edge (API 36+), getBoundingClientRect() may report
22964
+ * inconsistent values. Sheet and card modals avoid this by using configuration-based
22965
+ * prediction instead. Regular modals use coordinate detection which works reliably
22966
+ * on web and iOS; Android edge-to-edge may need a configuration-based fallback
22967
+ * once a reliable detection mechanism is available.
22968
+ *
22969
+ * @param wrapperEl - The modal wrapper element to measure
22970
+ * @returns SafeAreaConfig based on position
22971
+ */
22972
+ const getPositionBasedSafeAreaConfig = (wrapperEl) => {
22973
+ var _a, _b;
22974
+ const rect = wrapperEl.getBoundingClientRect();
22975
+ const vh = (_a = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerHeight) !== null && _a !== void 0 ? _a : 0;
22976
+ const vw = (_b = win$1 === null || win$1 === void 0 ? void 0 : win$1.innerWidth) !== null && _b !== void 0 ? _b : 0;
22977
+ // Only apply safe-area to sides where modal overlaps with screen edge
22978
+ return {
22979
+ top: rect.top <= EDGE_THRESHOLD ? 'inherit' : '0px',
22980
+ bottom: rect.bottom >= vh - EDGE_THRESHOLD ? 'inherit' : '0px',
22981
+ left: rect.left <= EDGE_THRESHOLD ? 'inherit' : '0px',
22982
+ right: rect.right >= vw - EDGE_THRESHOLD ? 'inherit' : '0px',
22983
+ };
22984
+ };
22985
+ /**
22986
+ * Applies safe-area CSS custom property overrides to the modal host element.
22987
+ *
22988
+ * @param hostEl - The modal host element (ion-modal)
22989
+ * @param config - Safe-area configuration to apply
22990
+ */
22991
+ const applySafeAreaOverrides = (hostEl, config) => {
22992
+ hostEl.style.setProperty('--ion-safe-area-top', config.top);
22993
+ hostEl.style.setProperty('--ion-safe-area-bottom', config.bottom);
22994
+ hostEl.style.setProperty('--ion-safe-area-left', config.left);
22995
+ hostEl.style.setProperty('--ion-safe-area-right', config.right);
22996
+ };
22997
+ /**
22998
+ * Clears safe-area CSS custom property overrides from the modal host element.
22999
+ *
23000
+ * @param hostEl - The modal host element (ion-modal)
23001
+ */
23002
+ const clearSafeAreaOverrides = (hostEl) => {
23003
+ hostEl.style.removeProperty('--ion-safe-area-top');
23004
+ hostEl.style.removeProperty('--ion-safe-area-bottom');
23005
+ hostEl.style.removeProperty('--ion-safe-area-left');
23006
+ hostEl.style.removeProperty('--ion-safe-area-right');
23007
+ };
22808
23008
 
22809
- const modalMdCss = ":host{--width:100%;--min-width:auto;--max-width:auto;--height:100%;--min-height:auto;--max-height:auto;--overflow:hidden;--border-radius:0;--border-width:0;--border-style:none;--border-color:transparent;--background:var(--ion-background-color, #fff);--box-shadow:none;--backdrop-opacity:0;left:0;right:0;top:0;bottom:0;display:-ms-flexbox;display:flex;position:absolute;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;outline:none;color:var(--ion-text-color, #000);contain:strict}.modal-wrapper,ion-backdrop{pointer-events:auto}:host(.overlay-hidden){display:none}.modal-wrapper,.modal-shadow{border-radius:var(--border-radius);width:var(--width);min-width:var(--min-width);max-width:var(--max-width);height:var(--height);min-height:var(--min-height);max-height:var(--max-height);border-width:var(--border-width);border-style:var(--border-style);border-color:var(--border-color);background:var(--background);-webkit-box-shadow:var(--box-shadow);box-shadow:var(--box-shadow);overflow:var(--overflow);z-index:10}.modal-shadow{position:absolute;background:transparent}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--width:600px;--height:500px;--ion-safe-area-top:0px;--ion-safe-area-bottom:0px;--ion-safe-area-right:0px;--ion-safe-area-left:0px}}@media only screen and (min-width: 768px) and (min-height: 768px){:host{--width:600px;--height:600px}}.modal-handle{left:0px;right:0px;top:5px;border-radius:8px;-webkit-margin-start:auto;margin-inline-start:auto;-webkit-margin-end:auto;margin-inline-end:auto;position:absolute;width:36px;height:5px;-webkit-transform:translateZ(0);transform:translateZ(0);border:0;background:var(--ion-color-step-350, var(--ion-background-color-step-350, #c0c0be));cursor:pointer;z-index:11}.modal-handle::before{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px;padding-top:4px;padding-bottom:4px;position:absolute;width:36px;height:5px;-webkit-transform:translate(-50%, -50%);transform:translate(-50%, -50%);content:\"\"}:host(.modal-sheet){--height:calc(100% - (var(--ion-safe-area-top) + 10px))}:host(.modal-sheet) .modal-wrapper,:host(.modal-sheet) .modal-shadow{position:absolute;bottom:0}:host(.modal-sheet.modal-no-expand-scroll) ion-footer{position:absolute;bottom:0;width:var(--width)}:host{--backdrop-opacity:var(--ion-backdrop-opacity, 0.32)}@media only screen and (min-width: 768px) and (min-height: 600px){:host{--border-radius:2px;--box-shadow:0 28px 48px rgba(0, 0, 0, 0.4)}}.modal-wrapper{-webkit-transform:translate3d(0, 40px, 0);transform:translate3d(0, 40px, 0);opacity:0.01}";
23009
+ 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}";
23010
+
23011
+ 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}";
22810
23012
 
22811
23013
  // TODO(FW-2832): types
22812
23014
  /**
@@ -23029,13 +23231,31 @@ class Modal {
23029
23231
  }
23030
23232
  }
23031
23233
  onWindowResize() {
23032
- // Only handle resize for iOS card modals when no custom animations are provided
23033
- if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
23234
+ if (!this.presented)
23034
23235
  return;
23035
- }
23036
23236
  clearTimeout(this.resizeTimeout);
23037
23237
  this.resizeTimeout = setTimeout(() => {
23038
- this.handleViewTransition();
23238
+ const context = this.getSafeAreaContext();
23239
+ // iOS card modals: handle portrait/landscape view transitions
23240
+ if (context.isCardModal && !this.enterAnimation && !this.leaveAnimation) {
23241
+ this.handleViewTransition();
23242
+ }
23243
+ // Sheet modals: re-compute the internal offset property since safe-area
23244
+ // values may change on device rotation (e.g., portrait notch vs landscape).
23245
+ if (context.isSheetModal) {
23246
+ this.updateSheetOffsetTop();
23247
+ }
23248
+ // Regular (non-sheet, non-card) modals: update safe-area overrides
23249
+ // since the viewport may have crossed the centered-dialog breakpoint.
23250
+ if (!context.isSheetModal && !context.isCardModal) {
23251
+ this.updateSafeAreaOverrides();
23252
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
23253
+ if (this.wrapperEl) {
23254
+ this.wrapperEl.style.removeProperty('height');
23255
+ this.wrapperEl.style.removeProperty('padding-bottom');
23256
+ }
23257
+ this.applyFullscreenSafeArea();
23258
+ }
23039
23259
  }, 50); // Debounce to avoid excessive calls during active resizing
23040
23260
  }
23041
23261
  breakpointsChanged(breakpoints) {
@@ -23052,6 +23272,9 @@ class Modal {
23052
23272
  this.triggerController.removeClickListener();
23053
23273
  this.cleanupViewTransitionListener();
23054
23274
  this.cleanupParentRemovalObserver();
23275
+ // Also called in dismiss() — intentional dual cleanup covers both
23276
+ // dismiss-then-remove and direct DOM removal without dismiss.
23277
+ this.cleanupSafeAreaOverrides();
23055
23278
  }
23056
23279
  componentWillLoad() {
23057
23280
  var _a;
@@ -23212,6 +23435,11 @@ class Modal {
23212
23435
  await waitForMount();
23213
23436
  }
23214
23437
  writeTask(() => this.el.classList.add('show-modal'));
23438
+ // Recalculate isSheetModal before safe-area setup because framework
23439
+ // bindings (e.g., Angular) may not have been applied when componentWillLoad ran.
23440
+ this.isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23441
+ // Set initial safe-area overrides before animation
23442
+ this.setInitialSafeAreaOverrides();
23215
23443
  const hasCardModal = presentingElement !== undefined;
23216
23444
  /**
23217
23445
  * We need to change the status bar at the
@@ -23229,6 +23457,10 @@ class Modal {
23229
23457
  backdropBreakpoint: this.backdropBreakpoint,
23230
23458
  expandToScroll: this.expandToScroll,
23231
23459
  });
23460
+ // Update safe-area based on actual position after animation
23461
+ this.updateSafeAreaOverrides();
23462
+ // Apply fullscreen safe-area padding if needed
23463
+ this.applyFullscreenSafeArea();
23232
23464
  /* tslint:disable-next-line */
23233
23465
  if (typeof window !== 'undefined') {
23234
23466
  /**
@@ -23260,13 +23492,7 @@ class Modal {
23260
23492
  };
23261
23493
  window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
23262
23494
  }
23263
- /**
23264
- * Recalculate isSheetModal because framework bindings (e.g., Angular)
23265
- * may not have been applied when componentWillLoad ran.
23266
- */
23267
- const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
23268
- this.isSheetModal = isSheetModal;
23269
- if (isSheetModal) {
23495
+ if (this.isSheetModal) {
23270
23496
  this.initSheetGesture();
23271
23497
  }
23272
23498
  else if (hasCardModal) {
@@ -23456,6 +23682,9 @@ class Modal {
23456
23682
  if (this.gestureAnimationDismissing && role !== GESTURE) {
23457
23683
  return false;
23458
23684
  }
23685
+ // Cancel any pending resize timeout to prevent stale updates during dismiss
23686
+ clearTimeout(this.resizeTimeout);
23687
+ this.resizeTimeout = undefined;
23459
23688
  /**
23460
23689
  * Because the canDismiss check below is async,
23461
23690
  * we need to claim a lock before the check happens,
@@ -23510,6 +23739,7 @@ class Modal {
23510
23739
  }
23511
23740
  this.cleanupViewTransitionListener();
23512
23741
  this.cleanupParentRemovalObserver();
23742
+ this.cleanupSafeAreaOverrides();
23513
23743
  this.cleanupChildRoutePassthrough();
23514
23744
  }
23515
23745
  this.currentBreakpoint = undefined;
@@ -23630,6 +23860,9 @@ class Modal {
23630
23860
  this.viewTransitionAnimation = transitionAnimation;
23631
23861
  transitionAnimation.play().then(() => {
23632
23862
  this.viewTransitionAnimation = undefined;
23863
+ // Wait for a layout pass after the transition so getBoundingClientRect()
23864
+ // in getPositionBasedSafeAreaConfig() reflects the new dimensions.
23865
+ raf(() => this.updateSafeAreaOverrides());
23633
23866
  // After orientation transition, recreate the swipe-to-close gesture
23634
23867
  // with updated animation that reflects the new presenting element state
23635
23868
  this.reinitSwipeToClose();
@@ -23756,6 +23989,123 @@ class Modal {
23756
23989
  (_a = this.parentRemovalObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
23757
23990
  this.parentRemovalObserver = undefined;
23758
23991
  }
23992
+ /**
23993
+ * Creates the context object for safe-area utilities.
23994
+ */
23995
+ getSafeAreaContext() {
23996
+ return {
23997
+ isSheetModal: this.isSheetModal,
23998
+ isCardModal: this.presentingElement !== undefined && getIonMode$1(this) === 'ios',
23999
+ presentingElement: this.presentingElement,
24000
+ breakpoints: this.breakpoints,
24001
+ currentBreakpoint: this.currentBreakpoint,
24002
+ };
24003
+ }
24004
+ /**
24005
+ * Sets initial safe-area overrides before modal animation.
24006
+ * Called in present() before animation starts.
24007
+ *
24008
+ * For sheet modals, the SCSS --height formula uses --ion-modal-offset-top
24009
+ * (an internal property) instead of --ion-safe-area-top. We resolve the
24010
+ * root safe-area-top to pixels and set --ion-modal-offset-top, decoupling
24011
+ * the height calculation from --ion-safe-area-top (which is zeroed for
24012
+ * sheets to prevent header content from getting double-offset padding).
24013
+ */
24014
+ setInitialSafeAreaOverrides() {
24015
+ const context = this.getSafeAreaContext();
24016
+ const safeAreaConfig = getInitialSafeAreaConfig(context);
24017
+ applySafeAreaOverrides(this.el, safeAreaConfig);
24018
+ // Set the internal offset property with the resolved root safe-area-top value
24019
+ if (context.isSheetModal) {
24020
+ this.updateSheetOffsetTop();
24021
+ }
24022
+ }
24023
+ /**
24024
+ * Resolves the current root --ion-safe-area-top value and sets the
24025
+ * internal --ion-modal-offset-top property on the host element.
24026
+ * Called on present and on resize (e.g., device rotation changes safe-area).
24027
+ */
24028
+ updateSheetOffsetTop() {
24029
+ const safeAreaTop = getRootSafeAreaTop();
24030
+ this.el.style.setProperty('--ion-modal-offset-top', `${safeAreaTop}px`);
24031
+ }
24032
+ /**
24033
+ * Updates safe-area overrides during dynamic state changes.
24034
+ * Called after animations, during gestures, and on orientation changes.
24035
+ */
24036
+ updateSafeAreaOverrides() {
24037
+ const { wrapperEl, el } = this;
24038
+ const context = this.getSafeAreaContext();
24039
+ // Sheet modals: safe-area is fully determined at presentation time
24040
+ // (top is always 0px, height is frozen). Nothing to update.
24041
+ if (context.isSheetModal)
24042
+ return;
24043
+ // Card modals have fixed safe-area requirements set by initial prediction.
24044
+ if (context.isCardModal)
24045
+ return;
24046
+ // wrapperEl is required for position-based detection below
24047
+ if (!wrapperEl)
24048
+ return;
24049
+ // Regular modals: use position-based detection to correctly handle both
24050
+ // fullscreen modals and centered dialogs with custom dimensions.
24051
+ const safeAreaConfig = getPositionBasedSafeAreaConfig(wrapperEl);
24052
+ applySafeAreaOverrides(el, safeAreaConfig);
24053
+ }
24054
+ /**
24055
+ * Applies padding-bottom to fullscreen modal wrapper to prevent
24056
+ * content from overlapping system navigation bar.
24057
+ */
24058
+ applyFullscreenSafeArea() {
24059
+ const { wrapperEl, el } = this;
24060
+ if (!wrapperEl)
24061
+ return;
24062
+ const context = this.getSafeAreaContext();
24063
+ if (context.isSheetModal || context.isCardModal)
24064
+ return;
24065
+ // Check for standard Ionic layout children (ion-content, ion-footer),
24066
+ // searching one level deep for wrapped components (e.g.,
24067
+ // <app-footer><ion-footer>...</ion-footer></app-footer>).
24068
+ // Note: uses a manual loop instead of querySelector(':scope > ...') because
24069
+ // Stencil's mock-doc (used in spec tests) does not support :scope.
24070
+ let hasContent = false;
24071
+ let hasFooter = false;
24072
+ for (const child of Array.from(el.children)) {
24073
+ if (child.tagName === 'ION-CONTENT')
24074
+ hasContent = true;
24075
+ if (child.tagName === 'ION-FOOTER')
24076
+ hasFooter = true;
24077
+ for (const grandchild of Array.from(child.children)) {
24078
+ if (grandchild.tagName === 'ION-CONTENT')
24079
+ hasContent = true;
24080
+ if (grandchild.tagName === 'ION-FOOTER')
24081
+ hasFooter = true;
24082
+ }
24083
+ }
24084
+ // Only apply wrapper padding for standard Ionic layouts (has ion-content
24085
+ // but no ion-footer). Custom modals with raw HTML are fully
24086
+ // developer-controlled and should not be modified.
24087
+ if (!hasContent || hasFooter)
24088
+ return;
24089
+ // Reduce wrapper height by safe-area and add equivalent padding so the
24090
+ // total visual size stays the same but the flex content area shrinks.
24091
+ // Using height + padding instead of box-sizing: border-box avoids
24092
+ // breaking custom modals that set --border-width (border-box would
24093
+ // include the border inside the height, changing the layout).
24094
+ wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
24095
+ wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
24096
+ }
24097
+ /**
24098
+ * Clears all safe-area overrides and padding from wrapper.
24099
+ */
24100
+ cleanupSafeAreaOverrides() {
24101
+ clearSafeAreaOverrides(this.el);
24102
+ // Remove internal sheet offset property
24103
+ this.el.style.removeProperty('--ion-modal-offset-top');
24104
+ if (this.wrapperEl) {
24105
+ this.wrapperEl.style.removeProperty('height');
24106
+ this.wrapperEl.style.removeProperty('padding-bottom');
24107
+ }
24108
+ }
23759
24109
  render() {
23760
24110
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
23761
24111
  const showHandle = handle !== false && isSheetModal;
@@ -23763,20 +24113,20 @@ class Modal {
23763
24113
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23764
24114
  const isHandleCycle = handleBehavior === 'cycle';
23765
24115
  const isSheetModalWithHandle = isSheetModal && showHandle;
23766
- return (hAsync(Host, Object.assign({ key: '978ba9cc81464b2d9b4caaf36285d5f7bf7e568d', "no-router": true,
24116
+ return (hAsync(Host, Object.assign({ key: '952cbd52d1fc737332a23d523b02d5fa60abf30a', "no-router": true,
23767
24117
  // Allow the modal to be navigable when the handle is focusable
23768
24118
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23769
24119
  zIndex: `${20000 + this.overlayIndex}`,
23770
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), hAsync("ion-backdrop", { key: '9f95566d8e0c06bb9607f514484848c250f35f1f', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '91ff26d4f3ab4367f3a2940f51c6a5a3d4c972c3', class: "modal-shadow" }), hAsync("div", Object.assign({ key: '064b2dba13854505a4eb1f5e54296ac79cb7a2bd',
24120
+ }, 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: '66e3658cfcc2961d99ecc1174f26b1794ceb65e9', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '3d1b9b26daf87374e2029d0e0a7fc09b9257fd77', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'e652c2b3eb66afa8367d94c3b14ba0e825877a82',
23771
24121
  /*
23772
24122
  role and aria-modal must be used on the
23773
24123
  same element. They must also be set inside the
23774
24124
  shadow DOM otherwise ion-button will not be highlighted
23775
24125
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23776
24126
  */
23777
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: 'b579b08b114022032d078a527111456ceefc7b35', class: "modal-handle",
24127
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '41662183450e8ef2f7d96dac2bea5952bad8b7a5', class: "modal-handle",
23778
24128
  // Prevents the handle from receiving keyboard focus when it does not cycle
23779
- tabIndex: !isHandleCycle ? -1 : 0, "aria-label": "Activate to adjust the size of the dialog overlaying the screen", onClick: isHandleCycle ? this.onHandleClick : undefined, part: "handle", ref: (el) => (this.dragHandleEl = el) })), hAsync("slot", { key: 'dacea58475b72d9c0209934ff31f3be6b9ac8c85', onSlotchange: this.onSlotChange }))));
24129
+ 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: '4aac4e2928929b9e5e59b33c83395182105da529', onSlotchange: this.onSlotChange }))));
23780
24130
  }
23781
24131
  get el() { return getElement(this); }
23782
24132
  static get watchers() { return {
@@ -26710,6 +27060,58 @@ class PickerColumnOption {
26710
27060
  }; }
26711
27061
  }
26712
27062
 
27063
+ /**
27064
+ * Shared per-frame cache for safe-area insets. Avoids creating a temporary
27065
+ * DOM element and forcing a synchronous reflow on every call within the same
27066
+ * frame (e.g., multiple popovers presenting simultaneously). Invalidated
27067
+ * after the next animation frame so values stay fresh across orientation
27068
+ * changes and viewport resizes.
27069
+ */
27070
+ let cachedInsets = null;
27071
+ let cacheInvalidationScheduled = false;
27072
+ /**
27073
+ * Resolves the current --ion-safe-area-* CSS custom property values
27074
+ * to actual pixel numbers. A temporary element is needed because
27075
+ * getComputedStyle on :root returns the declared value of custom
27076
+ * properties (e.g. "env(safe-area-inset-top)") rather than a
27077
+ * resolved number. By assigning them to standard padding properties,
27078
+ * the browser resolves the value.
27079
+ *
27080
+ * Results are cached for the current frame to avoid repeated reflows.
27081
+ */
27082
+ const getSafeAreaInsets = (doc) => {
27083
+ if (cachedInsets !== null) {
27084
+ return cachedInsets;
27085
+ }
27086
+ if (doc.body === null) {
27087
+ return { top: 0, bottom: 0, left: 0, right: 0 };
27088
+ }
27089
+ const el = doc.createElement('div');
27090
+ el.style.cssText =
27091
+ 'position:fixed;visibility:hidden;pointer-events:none;top:0;left:0;' +
27092
+ 'padding-top:var(--ion-safe-area-top,0px);' +
27093
+ 'padding-bottom:var(--ion-safe-area-bottom,0px);' +
27094
+ 'padding-left:var(--ion-safe-area-left,0px);' +
27095
+ 'padding-right:var(--ion-safe-area-right,0px);';
27096
+ doc.body.appendChild(el);
27097
+ const style = getComputedStyle(el);
27098
+ const insets = {
27099
+ top: parseFloat(style.paddingTop) || 0,
27100
+ bottom: parseFloat(style.paddingBottom) || 0,
27101
+ left: parseFloat(style.paddingLeft) || 0,
27102
+ right: parseFloat(style.paddingRight) || 0,
27103
+ };
27104
+ el.remove();
27105
+ cachedInsets = insets;
27106
+ if (!cacheInvalidationScheduled) {
27107
+ cacheInvalidationScheduled = true;
27108
+ raf(() => {
27109
+ cachedInsets = null;
27110
+ cacheInvalidationScheduled = false;
27111
+ });
27112
+ }
27113
+ return insets;
27114
+ };
26713
27115
  /**
26714
27116
  * Returns the dimensions of the popover
26715
27117
  * arrow on `ios` mode. If arrow is disabled
@@ -27345,8 +27747,10 @@ const calculatePopoverCenterAlign = (side, triggerBoundingBox, contentWidth, con
27345
27747
  * Adjusts popover positioning coordinates
27346
27748
  * such that popover does not appear offscreen
27347
27749
  * or overlapping safe area bounds.
27750
+ *
27751
+ * @internal - This is not part of the public API.
27348
27752
  */
27349
- const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeAreaMargin, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
27753
+ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, contentOriginX, contentOriginY, triggerCoordinates, coordArrowTop = 0, coordArrowLeft = 0, arrowHeight = 0) => {
27350
27754
  let arrowTop = coordArrowTop;
27351
27755
  const arrowLeft = coordArrowLeft;
27352
27756
  let left = coordLeft;
@@ -27356,16 +27760,19 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27356
27760
  let originY = contentOriginY;
27357
27761
  let checkSafeAreaLeft = false;
27358
27762
  let checkSafeAreaRight = false;
27763
+ let checkSafeAreaTop = false;
27764
+ let checkSafeAreaBottom = false;
27359
27765
  const triggerTop = triggerCoordinates
27360
27766
  ? triggerCoordinates.top + triggerCoordinates.height
27361
27767
  : bodyHeight / 2 - contentHeight / 2;
27362
27768
  const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
27363
27769
  let addPopoverBottomClass = false;
27770
+ const hideArrow = false;
27364
27771
  /**
27365
27772
  * Adjust popover so it does not
27366
27773
  * go off the left of the screen.
27367
27774
  */
27368
- if (left < bodyPadding + safeAreaMargin) {
27775
+ if (left < bodyPadding + safeArea.left) {
27369
27776
  left = bodyPadding;
27370
27777
  checkSafeAreaLeft = true;
27371
27778
  originX = 'left';
@@ -27374,41 +27781,58 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27374
27781
  * go off the right of the screen.
27375
27782
  */
27376
27783
  }
27377
- else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) {
27784
+ else if (contentWidth + bodyPadding + left + safeArea.right > bodyWidth) {
27378
27785
  checkSafeAreaRight = true;
27379
27786
  left = bodyWidth - contentWidth - bodyPadding;
27380
27787
  originX = 'right';
27381
27788
  }
27382
27789
  /**
27383
27790
  * Adjust popover so it does not
27384
- * go off the top of the screen.
27791
+ * go off the bottom of the screen
27792
+ * or overlap the bottom safe area.
27385
27793
  * If popover is on the left or the right of
27386
27794
  * the trigger, then we should not adjust top
27387
27795
  * margins.
27388
27796
  */
27389
- if (triggerTop + triggerHeight + contentHeight > bodyHeight && (side === 'top' || side === 'bottom')) {
27390
- if (triggerTop - contentHeight > 0) {
27797
+ if (triggerTop + triggerHeight + contentHeight > bodyHeight - safeArea.bottom &&
27798
+ (side === 'top' || side === 'bottom')) {
27799
+ /**
27800
+ * Calculate where the popover top would be if flipped
27801
+ * above the trigger. Check whether that position clears
27802
+ * the top safe area with room for bodyPadding.
27803
+ */
27804
+ const idealFlipTop = triggerTop - contentHeight - triggerHeight - (arrowHeight - 1);
27805
+ if (idealFlipTop >= safeArea.top + bodyPadding) {
27391
27806
  /**
27392
- * While we strive to align the popover with the trigger
27393
- * on smaller screens this is not always possible. As a result,
27394
- * we adjust the popover up so that it does not hang
27395
- * off the bottom of the screen. However, we do not want to move
27396
- * the popover up so much that it goes off the top of the screen.
27397
- *
27398
- * We chose 12 here so that the popover position looks a bit nicer as
27399
- * it is not right up against the edge of the screen.
27807
+ * Popover fits above the trigger without overlapping
27808
+ * the top safe area. Use the ideal position directly —
27809
+ * no safe-area CSS vars needed since it clears both edges.
27400
27810
  */
27401
- top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
27811
+ top = idealFlipTop;
27402
27812
  arrowTop = top + contentHeight;
27403
27813
  originY = 'bottom';
27404
27814
  addPopoverBottomClass = true;
27405
- /**
27406
- * If not enough room for popover to appear
27407
- * above trigger, then cut it off.
27408
- */
27409
27815
  }
27410
27816
  else {
27817
+ /**
27818
+ * Can't flip above the trigger. Constrain the bottom
27819
+ * edge while keeping the top near the trigger. This
27820
+ * creates a scrollable area anchored to the trigger.
27821
+ */
27411
27822
  bottom = bodyPadding;
27823
+ checkSafeAreaBottom = true;
27824
+ /**
27825
+ * When the trigger is near the bottom of the screen,
27826
+ * the calculated top may be at or past the bottom
27827
+ * constraint, leaving zero visible height. In that
27828
+ * case, pull top up to the top safe area so the
27829
+ * popover fills the available space between safe areas.
27830
+ */
27831
+ const bottomEdge = bodyHeight - safeArea.bottom - bodyPadding;
27832
+ if (top >= bottomEdge) {
27833
+ top = safeArea.top + bodyPadding;
27834
+ checkSafeAreaTop = true;
27835
+ }
27412
27836
  }
27413
27837
  }
27414
27838
  return {
@@ -27419,9 +27843,12 @@ const calculateWindowAdjustment = (side, coordTop, coordLeft, bodyPadding, bodyW
27419
27843
  originY,
27420
27844
  checkSafeAreaLeft,
27421
27845
  checkSafeAreaRight,
27846
+ checkSafeAreaTop,
27847
+ checkSafeAreaBottom,
27422
27848
  arrowTop,
27423
27849
  arrowLeft,
27424
27850
  addPopoverBottomClass,
27851
+ hideArrow,
27425
27852
  };
27426
27853
  };
27427
27854
  const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
@@ -27451,6 +27878,15 @@ const shouldShowArrow = (side, didAdjustBounds = false, ev, trigger) => {
27451
27878
  };
27452
27879
 
27453
27880
  const POPOVER_IOS_BODY_PADDING = 5;
27881
+ /**
27882
+ * Minimum edge margin for iOS popovers ensures visual spacing from screen
27883
+ * edges on devices without safe areas (e.g., older iPhones without notches).
27884
+ * Previously this was a hardcoded `safeAreaMargin = 25` that served dual
27885
+ * purpose: safe-area avoidance AND visual spacing. Now that actual safe-area
27886
+ * insets are read dynamically, this floor preserves the visual spacing when
27887
+ * safe-area values are 0.
27888
+ */
27889
+ const POPOVER_IOS_MIN_EDGE_MARGIN = 25;
27454
27890
  /**
27455
27891
  * iOS Popover Enter Animation
27456
27892
  */
@@ -27476,8 +27912,16 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27476
27912
  };
27477
27913
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
27478
27914
  const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
27479
- const margin = size === 'cover' ? 0 : 25;
27480
- const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass, } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
27915
+ const rawSafeArea = getSafeAreaInsets(doc);
27916
+ const safeArea = size === 'cover'
27917
+ ? { top: 0, bottom: 0, left: 0, right: 0 }
27918
+ : {
27919
+ top: Math.max(rawSafeArea.top, POPOVER_IOS_MIN_EDGE_MARGIN),
27920
+ bottom: Math.max(rawSafeArea.bottom, POPOVER_IOS_MIN_EDGE_MARGIN),
27921
+ left: Math.max(rawSafeArea.left, POPOVER_IOS_MIN_EDGE_MARGIN),
27922
+ right: Math.max(rawSafeArea.right, POPOVER_IOS_MIN_EDGE_MARGIN),
27923
+ };
27924
+ const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, checkSafeAreaTop, checkSafeAreaBottom, arrowTop, arrowLeft, addPopoverBottomClass} = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, safeArea, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
27481
27925
  const baseAnimation = createAnimation();
27482
27926
  const backdropAnimation = createAnimation();
27483
27927
  const contentAnimation = createAnimation();
@@ -27508,10 +27952,14 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27508
27952
  baseEl.classList.add('popover-bottom');
27509
27953
  }
27510
27954
  if (bottom !== undefined) {
27511
- contentEl.style.setProperty('bottom', `${bottom}px`);
27955
+ let bottomValue = `${bottom}px`;
27956
+ if (checkSafeAreaBottom) {
27957
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
27958
+ }
27959
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27512
27960
  }
27513
- const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
27514
- const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
27961
+ const safeAreaLeft = ' + var(--ion-safe-area-left, 0px)';
27962
+ const safeAreaRight = ' - var(--ion-safe-area-right, 0px)';
27515
27963
  let leftValue = `${left}px`;
27516
27964
  if (checkSafeAreaLeft) {
27517
27965
  leftValue = `${left}px${safeAreaLeft}`;
@@ -27519,7 +27967,11 @@ const iosEnterAnimation$1 = (baseEl, opts) => {
27519
27967
  if (checkSafeAreaRight) {
27520
27968
  leftValue = `${left}px${safeAreaRight}`;
27521
27969
  }
27522
- contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
27970
+ let topValue = `${top}px`;
27971
+ if (checkSafeAreaTop) {
27972
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
27973
+ }
27974
+ contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
27523
27975
  contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
27524
27976
  contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
27525
27977
  if (arrowEl !== null) {
@@ -27595,7 +28047,23 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27595
28047
  };
27596
28048
  const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
27597
28049
  const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
27598
- const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
28050
+ // MD mode now applies safe-area insets (previously passed 0, ignoring all safe areas).
28051
+ // This is needed for Android edge-to-edge (API 36+) where system bars overlap content.
28052
+ const safeArea = size === 'cover' ? { top: 0, bottom: 0, left: 0, right: 0 } : getSafeAreaInsets(doc);
28053
+ 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);
28054
+ const safeAreaLeftCalc = ' + var(--ion-safe-area-left, 0px)';
28055
+ const safeAreaRightCalc = ' - var(--ion-safe-area-right, 0px)';
28056
+ let leftValue = `${left}px`;
28057
+ if (checkSafeAreaLeft) {
28058
+ leftValue = `${left}px${safeAreaLeftCalc}`;
28059
+ }
28060
+ if (checkSafeAreaRight) {
28061
+ leftValue = `${left}px${safeAreaRightCalc}`;
28062
+ }
28063
+ let topValue = `${top}px`;
28064
+ if (checkSafeAreaTop) {
28065
+ topValue = `${top}px + var(--ion-safe-area-top, 0px)`;
28066
+ }
27599
28067
  const baseAnimation = createAnimation();
27600
28068
  const backdropAnimation = createAnimation();
27601
28069
  const wrapperAnimation = createAnimation();
@@ -27612,13 +28080,17 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27612
28080
  contentAnimation
27613
28081
  .addElement(contentEl)
27614
28082
  .beforeStyles({
27615
- top: `calc(${top}px + var(--offset-y, 0px))`,
27616
- left: `calc(${left}px + var(--offset-x, 0px))`,
28083
+ top: `calc(${topValue} + var(--offset-y, 0px))`,
28084
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
27617
28085
  'transform-origin': `${originY} ${originX}`,
27618
28086
  })
27619
28087
  .beforeAddWrite(() => {
27620
28088
  if (bottom !== undefined) {
27621
- contentEl.style.setProperty('bottom', `${bottom}px`);
28089
+ let bottomValue = `${bottom}px`;
28090
+ if (checkSafeAreaBottom) {
28091
+ bottomValue = `${bottom}px + var(--ion-safe-area-bottom, 0px)`;
28092
+ }
28093
+ contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
27622
28094
  }
27623
28095
  })
27624
28096
  .fromTo('transform', 'scale(0.8)', 'scale(1)');
@@ -27630,7 +28102,7 @@ const mdEnterAnimation$1 = (baseEl, opts) => {
27630
28102
  if (size === 'cover') {
27631
28103
  baseEl.style.setProperty('--width', `${contentWidth}px`);
27632
28104
  }
27633
- if (originY === 'bottom') {
28105
+ if (addPopoverBottomClass) {
27634
28106
  baseEl.classList.add('popover-bottom');
27635
28107
  }
27636
28108
  })
@@ -28465,9 +28937,9 @@ class Radio {
28465
28937
  }
28466
28938
  let radioButtonIds = 0;
28467
28939
 
28468
- const radioGroupIosCss = "ion-radio-group{vertical-align:top}.radio-group-wrapper{display:inline}.radio-group-top{line-height:1.5}.radio-group-top .error-text{display:none;color:var(--ion-color-danger, #c5000f)}.radio-group-top .helper-text{display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d))}.ion-touched.ion-invalid .radio-group-top .error-text{display:block}.ion-touched.ion-invalid .radio-group-top .helper-text{display:none}ion-list .radio-group-top{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}";
28940
+ const radioGroupIosCss = "ion-radio-group{vertical-align:top}.radio-group-top{line-height:1.5}.radio-group-top .error-text{display:none;color:var(--ion-color-danger, #c5000f)}.radio-group-top .helper-text{display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d))}.ion-touched.ion-invalid .radio-group-top .error-text{display:block}.ion-touched.ion-invalid .radio-group-top .helper-text{display:none}ion-list .radio-group-top{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}";
28469
28941
 
28470
- const radioGroupMdCss = "ion-radio-group{vertical-align:top}.radio-group-wrapper{display:inline}.radio-group-top{line-height:1.5}.radio-group-top .error-text{display:none;color:var(--ion-color-danger, #c5000f)}.radio-group-top .helper-text{display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d))}.ion-touched.ion-invalid .radio-group-top .error-text{display:block}.ion-touched.ion-invalid .radio-group-top .helper-text{display:none}ion-list .radio-group-top{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}";
28942
+ const radioGroupMdCss = "ion-radio-group{vertical-align:top}.radio-group-top{line-height:1.5}.radio-group-top .error-text{display:none;color:var(--ion-color-danger, #c5000f)}.radio-group-top .helper-text{display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d))}.ion-touched.ion-invalid .radio-group-top .error-text{display:block}.ion-touched.ion-invalid .radio-group-top .helper-text{display:none}ion-list .radio-group-top{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}";
28471
28943
 
28472
28944
  class RadioGroup {
28473
28945
  constructor(hostRef) {
@@ -32367,11 +32839,14 @@ class Searchbar {
32367
32839
  * Clears the input field and triggers the control change.
32368
32840
  */
32369
32841
  this.onClearInput = async (shouldFocus) => {
32842
+ if (this.clearTimeout) {
32843
+ clearTimeout(this.clearTimeout);
32844
+ }
32370
32845
  this.ionClear.emit();
32371
32846
  return new Promise((resolve) => {
32372
32847
  // setTimeout() fixes https://github.com/ionic-team/ionic-framework/issues/7527
32373
32848
  // wait for 4 frames
32374
- setTimeout(() => {
32849
+ this.clearTimeout = setTimeout(() => {
32375
32850
  const value = this.getValue();
32376
32851
  if (value !== '') {
32377
32852
  this.value = '';
@@ -32503,10 +32978,18 @@ class Searchbar {
32503
32978
  this.originalIonInput = this.ionInput;
32504
32979
  this.positionElements();
32505
32980
  this.debounceChanged();
32506
- setTimeout(() => {
32981
+ this.loadTimeout = setTimeout(() => {
32507
32982
  this.noAnimate = false;
32508
32983
  }, 300);
32509
32984
  }
32985
+ disconnectedCallback() {
32986
+ if (this.loadTimeout) {
32987
+ clearTimeout(this.loadTimeout);
32988
+ }
32989
+ if (this.clearTimeout) {
32990
+ clearTimeout(this.clearTimeout);
32991
+ }
32992
+ }
32510
32993
  emitStyle() {
32511
32994
  this.ionStyle.emit({
32512
32995
  searchbar: true,
@@ -32695,8 +33178,8 @@ class Searchbar {
32695
33178
  const clearIcon = this.clearIcon || (mode === 'ios' ? closeCircle : closeSharp);
32696
33179
  const searchIcon = this.searchIcon || (mode === 'ios' ? searchOutline : searchSharp);
32697
33180
  const shouldShowCancelButton = this.shouldShowCancelButton();
32698
- const cancelButton = this.showCancelButton !== 'never' && (hAsync("button", { key: 'bf574336a561a5bf66c771fb606a9b19adbecb68', "aria-label": cancelButtonText, "aria-hidden": shouldShowCancelButton ? undefined : 'true', type: "button", tabIndex: mode === 'ios' && !shouldShowCancelButton ? -1 : undefined, onMouseDown: this.onCancelSearchbar, onTouchStart: this.onCancelSearchbar, class: "searchbar-cancel-button" }, hAsync("div", { key: '747ad4368bda5044198259d5bc8f7be9e472e5af', "aria-hidden": "true" }, mode === 'md' ? (hAsync("ion-icon", { "aria-hidden": "true", mode: mode, icon: this.cancelButtonIcon, lazy: false })) : (cancelButtonText))));
32699
- return (hAsync(Host, { key: 'bcc5b33a2859903ba11bfc5c611c6a2aaa71d06b', role: "search", "aria-disabled": this.disabled ? 'true' : null, class: createColorClasses$1(this.color, {
33181
+ const cancelButton = this.showCancelButton !== 'never' && (hAsync("button", { key: '9e9f3a7f6ee66140181f8c3cfa42bbeee75adafc', "aria-label": cancelButtonText, "aria-hidden": shouldShowCancelButton ? undefined : 'true', type: "button", tabIndex: mode === 'ios' && !shouldShowCancelButton ? -1 : undefined, onMouseDown: this.onCancelSearchbar, onTouchStart: this.onCancelSearchbar, class: "searchbar-cancel-button" }, hAsync("div", { key: '5f83fbcc0708830637815e8c0b69356cad381870', "aria-hidden": "true" }, mode === 'md' ? (hAsync("ion-icon", { "aria-hidden": "true", mode: mode, icon: this.cancelButtonIcon, lazy: false })) : (cancelButtonText))));
33182
+ return (hAsync(Host, { key: '219c263c1f3cc63e18bc2d2ba5d672b7806eddd0', role: "search", "aria-disabled": this.disabled ? 'true' : null, class: createColorClasses$1(this.color, {
32700
33183
  [mode]: true,
32701
33184
  'searchbar-animated': animated,
32702
33185
  'searchbar-disabled': this.disabled,
@@ -32706,14 +33189,14 @@ class Searchbar {
32706
33189
  'searchbar-has-focus': this.focused,
32707
33190
  'searchbar-should-show-clear': this.shouldShowClearButton(),
32708
33191
  'searchbar-should-show-cancel': this.shouldShowCancelButton(),
32709
- }) }, hAsync("div", { key: 'ac0dbf5ddd9c4eb6d714f45c62c44ba3d0bf034d', class: "searchbar-input-container" }, hAsync("input", Object.assign({ key: '4b9fdac1b0d5a7a68bf61a6beb00f166434d3e84', "aria-label": "search text", disabled: this.disabled, ref: (el) => (this.nativeInput = el), class: "searchbar-input", inputMode: this.inputmode, enterKeyHint: this.enterkeyhint, name: this.name, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, minLength: this.minlength, maxLength: this.maxlength, placeholder: this.placeholder, type: this.type, value: this.getValue(), autoCapitalize: autocapitalize === 'default' ? undefined : autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, spellcheck: this.spellcheck }, this.inheritedAttributes)), mode === 'md' && cancelButton, hAsync("ion-icon", { key: '32a889c48b3960560a3873061b4ac5a8a5ce532f', "aria-hidden": "true", mode: mode, icon: searchIcon, lazy: false, class: "searchbar-search-icon" }), hAsync("button", { key: '71222df691ef0b95008e619f755224d069ee9388', "aria-label": "reset", type: "button", "no-blur": true, class: "searchbar-clear-button", onPointerDown: (ev) => {
33192
+ }) }, hAsync("div", { key: '3f1c25442d6d87c0df67f79482a925b80dcc403d', class: "searchbar-input-container" }, hAsync("input", Object.assign({ key: 'a386fecafda0fab449ea1488906ba6cfc55e2b5c', "aria-label": "search text", disabled: this.disabled, ref: (el) => (this.nativeInput = el), class: "searchbar-input", inputMode: this.inputmode, enterKeyHint: this.enterkeyhint, name: this.name, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, minLength: this.minlength, maxLength: this.maxlength, placeholder: this.placeholder, type: this.type, value: this.getValue(), autoCapitalize: autocapitalize === 'default' ? undefined : autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect, spellcheck: this.spellcheck }, this.inheritedAttributes)), mode === 'md' && cancelButton, hAsync("ion-icon", { key: '5f81642628335def39a14f0726e0fafdc7203776', "aria-hidden": "true", mode: mode, icon: searchIcon, lazy: false, class: "searchbar-search-icon" }), hAsync("button", { key: 'da3073646f2d7f639261b556517fbadabc3c10e9', "aria-label": "reset", type: "button", "no-blur": true, class: "searchbar-clear-button", onPointerDown: (ev) => {
32710
33193
  /**
32711
33194
  * This prevents mobile browsers from
32712
33195
  * blurring the input when the clear
32713
33196
  * button is activated.
32714
33197
  */
32715
33198
  ev.preventDefault();
32716
- }, onClick: () => this.onClearInput(true) }, hAsync("ion-icon", { key: 'f1c8c0bd8fbb655085875731d3ece387dbe2967e', "aria-hidden": "true", mode: mode, icon: clearIcon, lazy: false, class: "searchbar-clear-icon" }))), mode === 'ios' && cancelButton));
33199
+ }, onClick: () => this.onClearInput(true) }, hAsync("ion-icon", { key: '065cd6c707ae462c0767df1bab04ce8045f340a4', "aria-hidden": "true", mode: mode, icon: clearIcon, lazy: false, class: "searchbar-clear-icon" }))), mode === 'ios' && cancelButton));
32717
33200
  }
32718
33201
  get el() { return getElement(this); }
32719
33202
  static get watchers() { return {