@ionic/core 8.7.17-dev.11767717752.14fe98a4 → 8.7.17-dev.11767796972.148e4bc4

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.
package/hydrate/index.js CHANGED
@@ -10622,15 +10622,38 @@ class Content {
10622
10622
  }
10623
10623
  }
10624
10624
  /**
10625
- * Detects sibling ion-header and ion-footer elements.
10626
- * When these are absent, content needs to handle safe-area padding directly.
10625
+ * Detects sibling ion-header and ion-footer elements and sets up
10626
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
10627
10627
  */
10628
10628
  detectSiblingElements() {
10629
- // Check parent element for sibling header/footer.
10629
+ this.updateSiblingDetection();
10630
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
10631
+ const parent = this.el.parentElement;
10632
+ if (parent && !this.parentMutationObserver) {
10633
+ this.parentMutationObserver = new MutationObserver(() => {
10634
+ this.updateSiblingDetection();
10635
+ });
10636
+ this.parentMutationObserver.observe(parent, { childList: true });
10637
+ }
10638
+ }
10639
+ /**
10640
+ * Updates hasHeader/hasFooter based on current DOM state.
10641
+ * Checks both direct siblings and elements wrapped in custom components
10642
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
10643
+ */
10644
+ updateSiblingDetection() {
10630
10645
  const parent = this.el.parentElement;
10631
10646
  if (parent) {
10647
+ // First check for direct ion-header/ion-footer siblings
10632
10648
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
10633
10649
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
10650
+ // If not found, check if any sibling contains them (wrapped components)
10651
+ if (!this.hasHeader) {
10652
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
10653
+ }
10654
+ if (!this.hasFooter) {
10655
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
10656
+ }
10634
10657
  }
10635
10658
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
10636
10659
  if (!this.hasFooter) {
@@ -10640,8 +10663,28 @@ class Content {
10640
10663
  }
10641
10664
  }
10642
10665
  }
10666
+ /**
10667
+ * Checks if any sibling element of ion-content contains the specified element.
10668
+ * Only searches one level deep to avoid finding elements in nested pages.
10669
+ */
10670
+ siblingContainsElement(parent, tagName) {
10671
+ for (const sibling of parent.children) {
10672
+ // Skip ion-content itself
10673
+ if (sibling === this.el)
10674
+ continue;
10675
+ // Check if this sibling contains the target element as an immediate child
10676
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
10677
+ return true;
10678
+ }
10679
+ }
10680
+ return false;
10681
+ }
10643
10682
  disconnectedCallback() {
10683
+ var _a;
10644
10684
  this.onScrollEnd();
10685
+ // Clean up mutation observer to prevent memory leaks
10686
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
10687
+ this.parentMutationObserver = undefined;
10645
10688
  if (hasLazyBuild(this.el)) {
10646
10689
  /**
10647
10690
  * The event listener and tabs caches need to
@@ -10876,7 +10919,7 @@ class Content {
10876
10919
  const forceOverscroll = this.shouldForceOverscroll();
10877
10920
  const transitionShadow = mode === 'ios';
10878
10921
  this.resize();
10879
- return (hAsync(Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10922
+ return (hAsync(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10880
10923
  [mode]: true,
10881
10924
  'content-sizing': hostContext('ion-popover', this.el),
10882
10925
  overscroll: forceOverscroll,
@@ -10886,12 +10929,12 @@ class Content {
10886
10929
  }), style: {
10887
10930
  '--offset-top': `${this.cTop}px`,
10888
10931
  '--offset-bottom': `${this.cBottom}px`,
10889
- } }, inheritedAttributes), hAsync("div", { key: '75d7cf9315bc8dfb150c3b5bcc356a1f9c793b5a', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: 'f68bc5843c93ed6f32e998b80c5edc553c77a2d7', class: {
10932
+ } }, inheritedAttributes), hAsync("div", { key: '4c0482cda885348eea9eb66d7f076af6b38c52e5', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: '6fbb39bf7ab7120009c56aea9340de45c934eeed', class: {
10890
10933
  'inner-scroll': true,
10891
10934
  'scroll-x': scrollX,
10892
10935
  'scroll-y': scrollY,
10893
10936
  overscroll: (scrollX || scrollY) && forceOverscroll,
10894
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '8d4b2be00f036f6a24bfa65e6324ca715dd93b60' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10937
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '6425bc84edbc0c5b1f2764a1d611df1b46628274' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10895
10938
  }
10896
10939
  get el() { return getElement(this); }
10897
10940
  static get style() { return contentCss; }
@@ -22828,6 +22871,8 @@ class Modal {
22828
22871
  this.gestureAnimationDismissing = false;
22829
22872
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
22830
22873
  this.skipSafeAreaCoordinateDetection = false;
22874
+ // Track previous safe-area state to avoid redundant DOM writes
22875
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
22831
22876
  this.presented = false;
22832
22877
  /** @internal */
22833
22878
  this.hasController = false;
@@ -23018,7 +23063,8 @@ class Modal {
23018
23063
  }
23019
23064
  }
23020
23065
  onWindowResize() {
23021
- // Update safe-area overrides for all modal types on resize
23066
+ // Invalidate safe-area cache on resize (device rotation may change values)
23067
+ this.cachedSafeAreas = undefined;
23022
23068
  this.updateSafeAreaOverrides();
23023
23069
  // Only handle view transition for iOS card modals when no custom animations are provided
23024
23070
  if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -23477,8 +23523,26 @@ class Modal {
23477
23523
  */
23478
23524
  applyFullscreenSafeArea() {
23479
23525
  this.skipSafeAreaCoordinateDetection = true;
23526
+ this.updateFooterPadding();
23527
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
23528
+ if (!this.footerObserver) {
23529
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
23530
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
23531
+ }
23532
+ }
23533
+ /**
23534
+ * Updates wrapper padding based on footer presence.
23535
+ * Called initially and when footer is dynamically added/removed.
23536
+ */
23537
+ updateFooterPadding() {
23538
+ if (!this.wrapperEl)
23539
+ return;
23480
23540
  const hasFooter = this.el.querySelector('ion-footer') !== null;
23481
- if (!hasFooter && this.wrapperEl) {
23541
+ if (hasFooter) {
23542
+ this.wrapperEl.style.removeProperty('padding-bottom');
23543
+ this.wrapperEl.style.removeProperty('box-sizing');
23544
+ }
23545
+ else {
23482
23546
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23483
23547
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
23484
23548
  }
@@ -23496,21 +23560,26 @@ class Modal {
23496
23560
  }
23497
23561
  /**
23498
23562
  * Gets the root safe-area values from the document element.
23499
- * These represent the actual device safe areas before any overlay overrides.
23500
- */
23501
- getRootSafeAreaValues() {
23502
- const rootStyle = getComputedStyle(document.documentElement);
23503
- return {
23504
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23505
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23506
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23507
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23508
- };
23563
+ * Uses cached values during gestures to avoid getComputedStyle calls.
23564
+ */
23565
+ getSafeAreaValues() {
23566
+ if (!this.cachedSafeAreas) {
23567
+ const rootStyle = getComputedStyle(document.documentElement);
23568
+ this.cachedSafeAreas = {
23569
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23570
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23571
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23572
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23573
+ };
23574
+ }
23575
+ return this.cachedSafeAreas;
23509
23576
  }
23510
23577
  /**
23511
23578
  * Updates safe-area CSS variable overrides based on whether the modal
23512
23579
  * extends into each safe-area region. Called after animation
23513
23580
  * and during gestures to handle dynamic position changes.
23581
+ *
23582
+ * Optimized to avoid redundant DOM writes by tracking previous state.
23514
23583
  */
23515
23584
  updateSafeAreaOverrides() {
23516
23585
  if (this.skipSafeAreaCoordinateDetection) {
@@ -23521,20 +23590,34 @@ class Modal {
23521
23590
  return;
23522
23591
  }
23523
23592
  const rect = wrapper.getBoundingClientRect();
23524
- const safeAreas = this.getRootSafeAreaValues();
23593
+ const safeAreas = this.getSafeAreaValues();
23525
23594
  const extendsIntoTop = rect.top < safeAreas.top;
23526
23595
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
23527
23596
  const extendsIntoLeft = rect.left < safeAreas.left;
23528
23597
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
23598
+ // Only update DOM when state actually changes
23599
+ const prev = this.prevSafeAreaState;
23529
23600
  const style = this.el.style;
23530
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23531
- extendsIntoBottom
23532
- ? style.removeProperty('--ion-safe-area-bottom')
23533
- : style.setProperty('--ion-safe-area-bottom', '0px');
23534
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23535
- extendsIntoRight
23536
- ? style.removeProperty('--ion-safe-area-right')
23537
- : style.setProperty('--ion-safe-area-right', '0px');
23601
+ if (extendsIntoTop !== prev.top) {
23602
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23603
+ prev.top = extendsIntoTop;
23604
+ }
23605
+ if (extendsIntoBottom !== prev.bottom) {
23606
+ extendsIntoBottom
23607
+ ? style.removeProperty('--ion-safe-area-bottom')
23608
+ : style.setProperty('--ion-safe-area-bottom', '0px');
23609
+ prev.bottom = extendsIntoBottom;
23610
+ }
23611
+ if (extendsIntoLeft !== prev.left) {
23612
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23613
+ prev.left = extendsIntoLeft;
23614
+ }
23615
+ if (extendsIntoRight !== prev.right) {
23616
+ extendsIntoRight
23617
+ ? style.removeProperty('--ion-safe-area-right')
23618
+ : style.setProperty('--ion-safe-area-right', '0px');
23619
+ prev.right = extendsIntoRight;
23620
+ }
23538
23621
  }
23539
23622
  sheetOnDismiss() {
23540
23623
  /**
@@ -23566,7 +23649,7 @@ class Modal {
23566
23649
  * For example, `cancel` or `backdrop`.
23567
23650
  */
23568
23651
  async dismiss(data, role) {
23569
- var _a;
23652
+ var _a, _b;
23570
23653
  if (this.gestureAnimationDismissing && role !== GESTURE) {
23571
23654
  return false;
23572
23655
  }
@@ -23628,8 +23711,23 @@ class Modal {
23628
23711
  }
23629
23712
  this.currentBreakpoint = undefined;
23630
23713
  this.animation = undefined;
23631
- // Reset safe-area detection flag for potential re-presentation
23714
+ // Reset safe-area state for potential re-presentation
23632
23715
  this.skipSafeAreaCoordinateDetection = false;
23716
+ this.cachedSafeAreas = undefined;
23717
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
23718
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
23719
+ this.footerObserver = undefined;
23720
+ // Clear styles that may have been set for safe-area handling
23721
+ if (this.wrapperEl) {
23722
+ this.wrapperEl.style.removeProperty('padding-bottom');
23723
+ this.wrapperEl.style.removeProperty('box-sizing');
23724
+ }
23725
+ // Clear safe-area CSS variable overrides
23726
+ const style = this.el.style;
23727
+ style.removeProperty('--ion-safe-area-top');
23728
+ style.removeProperty('--ion-safe-area-bottom');
23729
+ style.removeProperty('--ion-safe-area-left');
23730
+ style.removeProperty('--ion-safe-area-right');
23633
23731
  unlock();
23634
23732
  return dismissed;
23635
23733
  }
@@ -23879,20 +23977,20 @@ class Modal {
23879
23977
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23880
23978
  const isHandleCycle = handleBehavior === 'cycle';
23881
23979
  const isSheetModalWithHandle = isSheetModal && showHandle;
23882
- return (hAsync(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
23980
+ return (hAsync(Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
23883
23981
  // Allow the modal to be navigable when the handle is focusable
23884
23982
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23885
23983
  zIndex: `${20000 + this.overlayIndex}`,
23886
- }, 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: '1b6850d9b9f6e8f3865b49e0a14399e2ef43a5d6', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'eab52c0ebccb820781e92392dc6fa90db93525d5', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'ab9448cabdf03a633319999771ce1ca1edce5699',
23984
+ }, 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: '125658fbb071960da3905854668078e15bce56da', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '4a63815ef165e5806fef85ef48bf509813ff55c9', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'cfc4b20354cbf2c0f873f6aee91c9b8f553de61d',
23887
23985
  /*
23888
23986
  role and aria-modal must be used on the
23889
23987
  same element. They must also be set inside the
23890
23988
  shadow DOM otherwise ion-button will not be highlighted
23891
23989
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23892
23990
  */
23893
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: 'b2b8c0a8d8add0d43e928dd3d3519f184551e62b', class: "modal-handle",
23991
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '33a4ee89bb8b6512883cb8756641c1e27fdb0ebc', class: "modal-handle",
23894
23992
  // Prevents the handle from receiving keyboard focus when it does not cycle
23895
- 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: 'b994f206765f7b340971d378f00999c0da334102', onSlotchange: this.onSlotChange }))));
23993
+ 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: '986fafb71234591c96e927f92b7330bb5c76fc2e', onSlotchange: this.onSlotChange }))));
23896
23994
  }
23897
23995
  get el() { return getElement(this); }
23898
23996
  static get watchers() { return {
package/hydrate/index.mjs CHANGED
@@ -10620,15 +10620,38 @@ class Content {
10620
10620
  }
10621
10621
  }
10622
10622
  /**
10623
- * Detects sibling ion-header and ion-footer elements.
10624
- * When these are absent, content needs to handle safe-area padding directly.
10623
+ * Detects sibling ion-header and ion-footer elements and sets up
10624
+ * a mutation observer to handle dynamic changes (e.g., conditional rendering).
10625
10625
  */
10626
10626
  detectSiblingElements() {
10627
- // Check parent element for sibling header/footer.
10627
+ this.updateSiblingDetection();
10628
+ // Watch for dynamic header/footer changes (common in React conditional rendering)
10629
+ const parent = this.el.parentElement;
10630
+ if (parent && !this.parentMutationObserver) {
10631
+ this.parentMutationObserver = new MutationObserver(() => {
10632
+ this.updateSiblingDetection();
10633
+ });
10634
+ this.parentMutationObserver.observe(parent, { childList: true });
10635
+ }
10636
+ }
10637
+ /**
10638
+ * Updates hasHeader/hasFooter based on current DOM state.
10639
+ * Checks both direct siblings and elements wrapped in custom components
10640
+ * (e.g., <my-header><ion-header>...</ion-header></my-header>).
10641
+ */
10642
+ updateSiblingDetection() {
10628
10643
  const parent = this.el.parentElement;
10629
10644
  if (parent) {
10645
+ // First check for direct ion-header/ion-footer siblings
10630
10646
  this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
10631
10647
  this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
10648
+ // If not found, check if any sibling contains them (wrapped components)
10649
+ if (!this.hasHeader) {
10650
+ this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
10651
+ }
10652
+ if (!this.hasFooter) {
10653
+ this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
10654
+ }
10632
10655
  }
10633
10656
  // If no footer found, check if we're inside ion-tabs which has ion-tab-bar
10634
10657
  if (!this.hasFooter) {
@@ -10638,8 +10661,28 @@ class Content {
10638
10661
  }
10639
10662
  }
10640
10663
  }
10664
+ /**
10665
+ * Checks if any sibling element of ion-content contains the specified element.
10666
+ * Only searches one level deep to avoid finding elements in nested pages.
10667
+ */
10668
+ siblingContainsElement(parent, tagName) {
10669
+ for (const sibling of parent.children) {
10670
+ // Skip ion-content itself
10671
+ if (sibling === this.el)
10672
+ continue;
10673
+ // Check if this sibling contains the target element as an immediate child
10674
+ if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
10675
+ return true;
10676
+ }
10677
+ }
10678
+ return false;
10679
+ }
10641
10680
  disconnectedCallback() {
10681
+ var _a;
10642
10682
  this.onScrollEnd();
10683
+ // Clean up mutation observer to prevent memory leaks
10684
+ (_a = this.parentMutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
10685
+ this.parentMutationObserver = undefined;
10643
10686
  if (hasLazyBuild(this.el)) {
10644
10687
  /**
10645
10688
  * The event listener and tabs caches need to
@@ -10874,7 +10917,7 @@ class Content {
10874
10917
  const forceOverscroll = this.shouldForceOverscroll();
10875
10918
  const transitionShadow = mode === 'ios';
10876
10919
  this.resize();
10877
- return (hAsync(Host, Object.assign({ key: '83665c0e35e4f4117709606e47d27ad36e343458', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10920
+ return (hAsync(Host, Object.assign({ key: 'c8e3a93e0b1ba6f7aa81a6a6065145ece9a6e2ef', role: isMainContent ? 'main' : undefined, class: createColorClasses$1(this.color, {
10878
10921
  [mode]: true,
10879
10922
  'content-sizing': hostContext('ion-popover', this.el),
10880
10923
  overscroll: forceOverscroll,
@@ -10884,12 +10927,12 @@ class Content {
10884
10927
  }), style: {
10885
10928
  '--offset-top': `${this.cTop}px`,
10886
10929
  '--offset-bottom': `${this.cBottom}px`,
10887
- } }, inheritedAttributes), hAsync("div", { key: '75d7cf9315bc8dfb150c3b5bcc356a1f9c793b5a', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: 'f68bc5843c93ed6f32e998b80c5edc553c77a2d7', class: {
10930
+ } }, inheritedAttributes), hAsync("div", { key: '4c0482cda885348eea9eb66d7f076af6b38c52e5', ref: (el) => (this.backgroundContentEl = el), id: "background-content", part: "background" }), fixedSlotPlacement === 'before' ? hAsync("slot", { name: "fixed" }) : null, hAsync("div", { key: '6fbb39bf7ab7120009c56aea9340de45c934eeed', class: {
10888
10931
  'inner-scroll': true,
10889
10932
  'scroll-x': scrollX,
10890
10933
  'scroll-y': scrollY,
10891
10934
  overscroll: (scrollX || scrollY) && forceOverscroll,
10892
- }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '8d4b2be00f036f6a24bfa65e6324ca715dd93b60' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10935
+ }, ref: (scrollEl) => (this.scrollEl = scrollEl), onScroll: this.scrollEvents ? (ev) => this.onScroll(ev) : undefined, part: "scroll" }, hAsync("slot", { key: '6425bc84edbc0c5b1f2764a1d611df1b46628274' })), transitionShadow ? (hAsync("div", { class: "transition-effect" }, hAsync("div", { class: "transition-cover" }), hAsync("div", { class: "transition-shadow" }))) : null, fixedSlotPlacement === 'after' ? hAsync("slot", { name: "fixed" }) : null));
10893
10936
  }
10894
10937
  get el() { return getElement(this); }
10895
10938
  static get style() { return contentCss; }
@@ -22826,6 +22869,8 @@ class Modal {
22826
22869
  this.gestureAnimationDismissing = false;
22827
22870
  // Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
22828
22871
  this.skipSafeAreaCoordinateDetection = false;
22872
+ // Track previous safe-area state to avoid redundant DOM writes
22873
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
22829
22874
  this.presented = false;
22830
22875
  /** @internal */
22831
22876
  this.hasController = false;
@@ -23016,7 +23061,8 @@ class Modal {
23016
23061
  }
23017
23062
  }
23018
23063
  onWindowResize() {
23019
- // Update safe-area overrides for all modal types on resize
23064
+ // Invalidate safe-area cache on resize (device rotation may change values)
23065
+ this.cachedSafeAreas = undefined;
23020
23066
  this.updateSafeAreaOverrides();
23021
23067
  // Only handle view transition for iOS card modals when no custom animations are provided
23022
23068
  if (getIonMode$1(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
@@ -23475,8 +23521,26 @@ class Modal {
23475
23521
  */
23476
23522
  applyFullscreenSafeArea() {
23477
23523
  this.skipSafeAreaCoordinateDetection = true;
23524
+ this.updateFooterPadding();
23525
+ // Watch for dynamic footer additions/removals (e.g., async data loading)
23526
+ if (!this.footerObserver) {
23527
+ this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
23528
+ this.footerObserver.observe(this.el, { childList: true, subtree: true });
23529
+ }
23530
+ }
23531
+ /**
23532
+ * Updates wrapper padding based on footer presence.
23533
+ * Called initially and when footer is dynamically added/removed.
23534
+ */
23535
+ updateFooterPadding() {
23536
+ if (!this.wrapperEl)
23537
+ return;
23478
23538
  const hasFooter = this.el.querySelector('ion-footer') !== null;
23479
- if (!hasFooter && this.wrapperEl) {
23539
+ if (hasFooter) {
23540
+ this.wrapperEl.style.removeProperty('padding-bottom');
23541
+ this.wrapperEl.style.removeProperty('box-sizing');
23542
+ }
23543
+ else {
23480
23544
  this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
23481
23545
  this.wrapperEl.style.setProperty('box-sizing', 'border-box');
23482
23546
  }
@@ -23494,21 +23558,26 @@ class Modal {
23494
23558
  }
23495
23559
  /**
23496
23560
  * Gets the root safe-area values from the document element.
23497
- * These represent the actual device safe areas before any overlay overrides.
23498
- */
23499
- getRootSafeAreaValues() {
23500
- const rootStyle = getComputedStyle(document.documentElement);
23501
- return {
23502
- top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23503
- bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23504
- left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23505
- right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23506
- };
23561
+ * Uses cached values during gestures to avoid getComputedStyle calls.
23562
+ */
23563
+ getSafeAreaValues() {
23564
+ if (!this.cachedSafeAreas) {
23565
+ const rootStyle = getComputedStyle(document.documentElement);
23566
+ this.cachedSafeAreas = {
23567
+ top: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-top')) || 0,
23568
+ bottom: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-bottom')) || 0,
23569
+ left: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-left')) || 0,
23570
+ right: parseFloat(rootStyle.getPropertyValue('--ion-safe-area-right')) || 0,
23571
+ };
23572
+ }
23573
+ return this.cachedSafeAreas;
23507
23574
  }
23508
23575
  /**
23509
23576
  * Updates safe-area CSS variable overrides based on whether the modal
23510
23577
  * extends into each safe-area region. Called after animation
23511
23578
  * and during gestures to handle dynamic position changes.
23579
+ *
23580
+ * Optimized to avoid redundant DOM writes by tracking previous state.
23512
23581
  */
23513
23582
  updateSafeAreaOverrides() {
23514
23583
  if (this.skipSafeAreaCoordinateDetection) {
@@ -23519,20 +23588,34 @@ class Modal {
23519
23588
  return;
23520
23589
  }
23521
23590
  const rect = wrapper.getBoundingClientRect();
23522
- const safeAreas = this.getRootSafeAreaValues();
23591
+ const safeAreas = this.getSafeAreaValues();
23523
23592
  const extendsIntoTop = rect.top < safeAreas.top;
23524
23593
  const extendsIntoBottom = rect.bottom > window.innerHeight - safeAreas.bottom;
23525
23594
  const extendsIntoLeft = rect.left < safeAreas.left;
23526
23595
  const extendsIntoRight = rect.right > window.innerWidth - safeAreas.right;
23596
+ // Only update DOM when state actually changes
23597
+ const prev = this.prevSafeAreaState;
23527
23598
  const style = this.el.style;
23528
- extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23529
- extendsIntoBottom
23530
- ? style.removeProperty('--ion-safe-area-bottom')
23531
- : style.setProperty('--ion-safe-area-bottom', '0px');
23532
- extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23533
- extendsIntoRight
23534
- ? style.removeProperty('--ion-safe-area-right')
23535
- : style.setProperty('--ion-safe-area-right', '0px');
23599
+ if (extendsIntoTop !== prev.top) {
23600
+ extendsIntoTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
23601
+ prev.top = extendsIntoTop;
23602
+ }
23603
+ if (extendsIntoBottom !== prev.bottom) {
23604
+ extendsIntoBottom
23605
+ ? style.removeProperty('--ion-safe-area-bottom')
23606
+ : style.setProperty('--ion-safe-area-bottom', '0px');
23607
+ prev.bottom = extendsIntoBottom;
23608
+ }
23609
+ if (extendsIntoLeft !== prev.left) {
23610
+ extendsIntoLeft ? style.removeProperty('--ion-safe-area-left') : style.setProperty('--ion-safe-area-left', '0px');
23611
+ prev.left = extendsIntoLeft;
23612
+ }
23613
+ if (extendsIntoRight !== prev.right) {
23614
+ extendsIntoRight
23615
+ ? style.removeProperty('--ion-safe-area-right')
23616
+ : style.setProperty('--ion-safe-area-right', '0px');
23617
+ prev.right = extendsIntoRight;
23618
+ }
23536
23619
  }
23537
23620
  sheetOnDismiss() {
23538
23621
  /**
@@ -23564,7 +23647,7 @@ class Modal {
23564
23647
  * For example, `cancel` or `backdrop`.
23565
23648
  */
23566
23649
  async dismiss(data, role) {
23567
- var _a;
23650
+ var _a, _b;
23568
23651
  if (this.gestureAnimationDismissing && role !== GESTURE) {
23569
23652
  return false;
23570
23653
  }
@@ -23626,8 +23709,23 @@ class Modal {
23626
23709
  }
23627
23710
  this.currentBreakpoint = undefined;
23628
23711
  this.animation = undefined;
23629
- // Reset safe-area detection flag for potential re-presentation
23712
+ // Reset safe-area state for potential re-presentation
23630
23713
  this.skipSafeAreaCoordinateDetection = false;
23714
+ this.cachedSafeAreas = undefined;
23715
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
23716
+ (_b = this.footerObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
23717
+ this.footerObserver = undefined;
23718
+ // Clear styles that may have been set for safe-area handling
23719
+ if (this.wrapperEl) {
23720
+ this.wrapperEl.style.removeProperty('padding-bottom');
23721
+ this.wrapperEl.style.removeProperty('box-sizing');
23722
+ }
23723
+ // Clear safe-area CSS variable overrides
23724
+ const style = this.el.style;
23725
+ style.removeProperty('--ion-safe-area-top');
23726
+ style.removeProperty('--ion-safe-area-bottom');
23727
+ style.removeProperty('--ion-safe-area-left');
23728
+ style.removeProperty('--ion-safe-area-right');
23631
23729
  unlock();
23632
23730
  return dismissed;
23633
23731
  }
@@ -23877,20 +23975,20 @@ class Modal {
23877
23975
  const isCardModal = presentingElement !== undefined && mode === 'ios';
23878
23976
  const isHandleCycle = handleBehavior === 'cycle';
23879
23977
  const isSheetModalWithHandle = isSheetModal && showHandle;
23880
- return (hAsync(Host, Object.assign({ key: '07ebca6a70eb99f8a2236e1d66a03097a7bb67d8', "no-router": true,
23978
+ return (hAsync(Host, Object.assign({ key: '11cd16cc481093a38a327abdd94467be3f71718d', "no-router": true,
23881
23979
  // Allow the modal to be navigable when the handle is focusable
23882
23980
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
23883
23981
  zIndex: `${20000 + this.overlayIndex}`,
23884
- }, 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: '1b6850d9b9f6e8f3865b49e0a14399e2ef43a5d6', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'eab52c0ebccb820781e92392dc6fa90db93525d5', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'ab9448cabdf03a633319999771ce1ca1edce5699',
23982
+ }, 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: '125658fbb071960da3905854668078e15bce56da', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '4a63815ef165e5806fef85ef48bf509813ff55c9', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'cfc4b20354cbf2c0f873f6aee91c9b8f553de61d',
23885
23983
  /*
23886
23984
  role and aria-modal must be used on the
23887
23985
  same element. They must also be set inside the
23888
23986
  shadow DOM otherwise ion-button will not be highlighted
23889
23987
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
23890
23988
  */
23891
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: 'b2b8c0a8d8add0d43e928dd3d3519f184551e62b', class: "modal-handle",
23989
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '33a4ee89bb8b6512883cb8756641c1e27fdb0ebc', class: "modal-handle",
23892
23990
  // Prevents the handle from receiving keyboard focus when it does not cycle
23893
- 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: 'b994f206765f7b340971d378f00999c0da334102', onSlotchange: this.onSlotChange }))));
23991
+ 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: '986fafb71234591c96e927f92b7330bb5c76fc2e', onSlotchange: this.onSlotChange }))));
23894
23992
  }
23895
23993
  get el() { return getElement(this); }
23896
23994
  static get watchers() { return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ionic/core",
3
- "version": "8.7.17-dev.11767717752.14fe98a4",
3
+ "version": "8.7.17-dev.11767796972.148e4bc4",
4
4
  "description": "Base components for Ionic",
5
5
  "engines": {
6
6
  "node": ">= 16"