voyager-ionic-core 8.8.4 → 8.8.5

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 (61) hide show
  1. package/components/ion-action-sheet.js +1 -1
  2. package/components/ion-content.js +1 -1
  3. package/components/ion-modal.js +1 -1
  4. package/components/ion-radio-group.js +1 -1
  5. package/components/ion-select-modal.js +1 -1
  6. package/components/ion-select-popover.js +1 -1
  7. package/components/ion-select.js +1 -1
  8. package/components/p-0z8QSI5b.js +4 -0
  9. package/components/{p-ApmKVjaE.js → p-BGHGpkPX.js} +1 -1
  10. package/components/p-BlNv564p.js +4 -0
  11. package/components/p-D-cP12ZN.js +4 -0
  12. package/components/p-D3Ti70Hx.js +4 -0
  13. package/components/{p-Bk2zuNWT.js → p-DvOO1fxp.js} +1 -1
  14. package/dist/cjs/ion-action-sheet.cjs.entry.js +4 -4
  15. package/dist/cjs/ion-app_8.cjs.entry.js +1 -1
  16. package/dist/cjs/ion-modal.cjs.entry.js +99 -45
  17. package/dist/cjs/ion-radio_2.cjs.entry.js +13 -1
  18. package/dist/cjs/ion-select-modal.cjs.entry.js +18 -7
  19. package/dist/cjs/ion-select_3.cjs.entry.js +18 -7
  20. package/dist/collection/components/action-sheet/action-sheet.js +4 -4
  21. package/dist/collection/components/content/content.css +1 -1
  22. package/dist/collection/components/modal/modal.js +73 -44
  23. package/dist/collection/components/modal/safe-area-utils.js +27 -2
  24. package/dist/collection/components/radio-group/radio-group.js +13 -1
  25. package/dist/collection/components/radio-group/test/fixtures.js +2 -2
  26. package/dist/collection/components/select-modal/select-modal.js +18 -7
  27. package/dist/collection/components/select-modal/test/fixtures.js +4 -0
  28. package/dist/collection/components/select-popover/select-popover.js +18 -7
  29. package/dist/collection/components/select-popover/test/fixtures.js +4 -0
  30. package/dist/docs.json +1 -1
  31. package/dist/esm/ion-action-sheet.entry.js +4 -4
  32. package/dist/esm/ion-app_8.entry.js +1 -1
  33. package/dist/esm/ion-modal.entry.js +99 -45
  34. package/dist/esm/ion-radio_2.entry.js +13 -1
  35. package/dist/esm/ion-select-modal.entry.js +18 -7
  36. package/dist/esm/ion-select_3.entry.js +18 -7
  37. package/dist/ionic/ionic.esm.js +1 -1
  38. package/dist/ionic/p-268a3397.entry.js +4 -0
  39. package/dist/ionic/p-28a9e720.entry.js +4 -0
  40. package/dist/ionic/{p-4dd5e8e0.entry.js → p-8fda6a62.entry.js} +1 -1
  41. package/dist/ionic/{p-9eac4eb1.entry.js → p-aa812c4b.entry.js} +1 -1
  42. package/dist/ionic/p-cb27fe68.entry.js +4 -0
  43. package/dist/ionic/p-ce2edb36.entry.js +4 -0
  44. package/dist/types/components/modal/modal.d.ts +41 -3
  45. package/dist/types/components/modal/safe-area-utils.d.ts +16 -0
  46. package/dist/types/components/radio-group/test/fixtures.d.ts +1 -1
  47. package/dist/types/components/select-modal/select-modal.d.ts +1 -0
  48. package/dist/types/components/select-modal/test/fixtures.d.ts +1 -0
  49. package/dist/types/components/select-popover/select-popover.d.ts +1 -0
  50. package/dist/types/components/select-popover/test/fixtures.d.ts +1 -0
  51. package/hydrate/index.js +153 -65
  52. package/hydrate/index.mjs +153 -65
  53. package/package.json +1 -1
  54. package/components/p-1KVKSLu5.js +0 -4
  55. package/components/p-BI7WNErr.js +0 -4
  56. package/components/p-BTF2nRLo.js +0 -4
  57. package/components/p-EK4xUz-q.js +0 -4
  58. package/dist/ionic/p-51c11c47.entry.js +0 -4
  59. package/dist/ionic/p-5681dde4.entry.js +0 -4
  60. package/dist/ionic/p-cb78f5a0.entry.js +0 -4
  61. package/dist/ionic/p-e6c5f060.entry.js +0 -4
package/hydrate/index.js CHANGED
@@ -9798,7 +9798,7 @@ class ActionSheet {
9798
9798
  if (isRadio) {
9799
9799
  htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
9800
9800
  }
9801
- return (hAsync("button", Object.assign({}, htmlAttrs, { role: isRadio ? 'radio' : undefined, type: "button", id: buttonId, class: Object.assign(Object.assign({}, buttonClass$3(b)), { 'action-sheet-selected': isActiveRadio }), onClick: () => {
9801
+ return (hAsync("button", Object.assign({}, htmlAttrs, { role: isRadio ? 'radio' : undefined, type: "button", id: buttonId, class: Object.assign(Object.assign({}, buttonClass$3(b)), (isRadio && { 'action-sheet-selected': isActiveRadio })), onClick: () => {
9802
9802
  if (isRadio) {
9803
9803
  this.selectRadioButton(b);
9804
9804
  }
@@ -9813,12 +9813,12 @@ class ActionSheet {
9813
9813
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
9814
9814
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
9815
9815
  const headerID = `action-sheet-${overlayIndex}-header`;
9816
- return (hAsync(Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
9816
+ return (hAsync(Host, Object.assign({ key: 'a56ee2ab59c763036140dbd10306a708c26e3c17', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
9817
9817
  zIndex: `${20000 + this.overlayIndex}`,
9818
- }, class: Object.assign(Object.assign({ [mode]: true }, getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), hAsync("ion-backdrop", { key: '521ede659f747864f6c974e09016436eceb7158c', tappable: this.backdropDismiss }), hAsync("div", { key: '7a7946fc434bc444f16a70638f5e948c69d33fcd', tabindex: "0", "aria-hidden": "true" }), hAsync("div", { key: 'bcff39a580489dbafa255842e57aa8602c6d0f18', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, hAsync("div", { key: '84bba13ce14261f0f0daa3f9c77648c9e7f36e0e', class: "action-sheet-container" }, hAsync("div", { key: 'd9c8ac404fd6719a7adf8cb36549f67616f9a0c4', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (hAsync("div", { key: '180433a8ad03ef5c54728a1a8f34715b6921d658', id: headerID, class: {
9818
+ }, class: Object.assign(Object.assign({ [mode]: true }, getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), hAsync("ion-backdrop", { key: 'c32eb4281fd6348c7d3989a3f509c211263048e6', tappable: this.backdropDismiss }), hAsync("div", { key: '7f0123114a876fc7cfff3cfb564aded4a7017797', tabindex: "0", "aria-hidden": "true" }), hAsync("div", { key: '645b1d5fde39a8907f21983d66e6ecb7a99aa05d', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, hAsync("div", { key: 'a78fb02848462d1a4f9356ac4fa1c43a2e5d90e4', class: "action-sheet-container" }, hAsync("div", { key: '5e846f53e067b211b985d6e1512b72b9d7c1a3aa', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (hAsync("div", { key: 'a90a0e096e1b2fa78b9adb9253c0a517f16e62cb', id: headerID, class: {
9819
9819
  'action-sheet-title': true,
9820
9820
  'action-sheet-has-sub-title': this.subHeader !== undefined,
9821
- } }, header, this.subHeader && hAsync("div", { key: '7138e79e61b1a8f42bc5a9175c57fa2f15d7ec5a', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (hAsync("div", { key: 'b617c722f5b8028d73ed34b69310f312c65f34a7', class: "action-sheet-group action-sheet-group-cancel" }, hAsync("button", Object.assign({ key: 'd0dd876fc48815df3710413c201c0b445a8e16c0' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass$3(cancelButton), onClick: () => this.buttonClick(cancelButton) }), hAsync("span", { key: 'e7b960157cc6fc5fe92a12090b2be55e8ae072e4', class: "action-sheet-button-inner" }, cancelButton.icon && (hAsync("ion-icon", { key: '05498ffc60cab911dbff0ecbc6168dea59ada9a5', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && hAsync("ion-ripple-effect", { key: '3d401346cea301be4ca03671f7370f6f4b0b6bde' })))))), hAsync("div", { key: '971f3c5fcc07f36c28eb469a47ec0290c692e139', tabindex: "0", "aria-hidden": "true" })));
9821
+ } }, header, this.subHeader && hAsync("div", { key: '40f00b12341625c548546de1885b9c9d93bc169c', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (hAsync("div", { key: 'ef6974cb63089623df08087274b82745443cee8c', class: "action-sheet-group action-sheet-group-cancel" }, hAsync("button", Object.assign({ key: 'b02911a6491d60f9dcb5da7d942392a9e96552c1' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass$3(cancelButton), onClick: () => this.buttonClick(cancelButton) }), hAsync("span", { key: '1187433e676eda55e52b5ae328a8e68bba22deb6', class: "action-sheet-button-inner" }, cancelButton.icon && (hAsync("ion-icon", { key: '079ab2a6bd40b996950053617f1c1c8207ecb1f1', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && hAsync("ion-ripple-effect", { key: '3bc473add8ac299f202f8c359d26708872c02f52' })))))), hAsync("div", { key: '9b1ae7b4e3649e9b85632f0d65627ca81499e68d', tabindex: "0", "aria-hidden": "true" })));
9822
9822
  }
9823
9823
  get el() { return getElement(this); }
9824
9824
  static get watchers() { return {
@@ -12367,7 +12367,7 @@ const isRTL$1 = (hostEl) => {
12367
12367
  return (document === null || document === void 0 ? void 0 : document.dir.toLowerCase()) === 'rtl';
12368
12368
  };
12369
12369
 
12370
- const contentCss = () => `:host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:""}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}`;
12370
+ const contentCss = () => `:host{--background:var(--ion-background-color, #fff);--color:var(--ion-text-color, #000);--padding-top:0px;--padding-bottom:0px;--padding-start:0px;--padding-end:0px;--keyboard-offset:0px;--offset-top:0px;--offset-bottom:0px;--overflow:auto;display:block;position:relative;-ms-flex:1;flex:1;width:100%;height:100%;margin:0 !important;padding:0 !important;font-family:var(--ion-font-family, inherit);contain:size style}:host(.ion-color) .inner-scroll{background:var(--ion-color-base);color:var(--ion-color-contrast)}#background-content{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);position:absolute;background:var(--background)}.inner-scroll{left:0px;right:0px;top:calc(var(--offset-top) * -1);bottom:calc(var(--offset-bottom) * -1);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:calc(var(--padding-top) + var(--offset-top));padding-bottom:calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom) + var(--ion-content-safe-area-padding-bottom, 0px));position:absolute;color:var(--color);-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden;-ms-touch-action:pan-x pan-y pinch-zoom;touch-action:pan-x pan-y pinch-zoom}.scroll-y,.scroll-x{-webkit-overflow-scrolling:touch;z-index:0;will-change:scroll-position}.scroll-y{overflow-y:var(--overflow);overscroll-behavior-y:contain}.scroll-x{overflow-x:var(--overflow);overscroll-behavior-x:contain}.overscroll::before,.overscroll::after{position:absolute;width:1px;height:1px;content:""}.overscroll::before{bottom:-1px}.overscroll::after{top:-1px}:host(.content-sizing){display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:0;contain:none}:host(.content-sizing) .inner-scroll{position:relative;top:0;bottom:0;margin-top:calc(var(--offset-top) * -1);margin-bottom:calc(var(--offset-bottom) * -1)}.transition-effect{display:none;position:absolute;width:100%;height:100vh;opacity:0;pointer-events:none}:host(.content-ltr) .transition-effect{left:-100%;}:host(.content-rtl) .transition-effect{right:-100%;}.transition-cover{position:absolute;right:0;width:100%;height:100%;background:black;opacity:0.1}.transition-shadow{display:block;position:absolute;width:100%;height:100%;-webkit-box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03);box-shadow:inset -9px 0 9px 0 rgba(0, 0, 100, 0.03)}:host(.content-ltr) .transition-shadow{right:0;}:host(.content-rtl) .transition-shadow{left:0;-webkit-transform:scaleX(-1);transform:scaleX(-1)}::slotted([slot=fixed]){position:absolute;-webkit-transform:translateZ(0);transform:translateZ(0)}`;
12371
12371
 
12372
12372
  /**
12373
12373
  * @slot - Content is placed in the scrollable area if provided without a slot.
@@ -25008,6 +25008,12 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
25008
25008
  const MODAL_INSET_MIN_WIDTH = 768;
25009
25009
  const MODAL_INSET_MIN_HEIGHT = 600;
25010
25010
  const EDGE_THRESHOLD = 5;
25011
+ /**
25012
+ * CSS values for `--width` / `--height` that are treated as fullscreen
25013
+ * (modal touches the corresponding screen edges). Empty string means the
25014
+ * property was not overridden. See `hasCustomModalDimensions()`.
25015
+ */
25016
+ const FULLSCREEN_SIZE_VALUES = new Set(['', '100%', '100vw', '100vh', '100dvw', '100dvh', '100svw', '100svh']);
25011
25017
  /**
25012
25018
  * Cache for resolved root safe-area-top value, invalidated once per frame.
25013
25019
  */
@@ -25056,6 +25062,22 @@ const getRootSafeAreaTop = () => {
25056
25062
  }
25057
25063
  return value;
25058
25064
  };
25065
+ /**
25066
+ * True when the modal host declares BOTH a non-fullscreen `--width` AND a
25067
+ * non-fullscreen `--height` (i.e. a centered-dialog-like modal that doesn't
25068
+ * touch any screen edge).
25069
+ *
25070
+ * The conservative "both axes" check avoids mis-zeroing safe-area for
25071
+ * partial-custom modals where the modal still touches top/bottom edges
25072
+ * (e.g. only `--width` overridden). Partial cases fall through to the
25073
+ * existing position-based post-animation correction.
25074
+ */
25075
+ const hasCustomModalDimensions = (hostEl) => {
25076
+ const styles = getComputedStyle(hostEl);
25077
+ const width = styles.getPropertyValue('--width').trim();
25078
+ const height = styles.getPropertyValue('--height').trim();
25079
+ return !FULLSCREEN_SIZE_VALUES.has(width) && !FULLSCREEN_SIZE_VALUES.has(height);
25080
+ };
25059
25081
  /**
25060
25082
  * Returns the initial safe-area configuration based on modal type.
25061
25083
  * This is called before animation starts and uses configuration-based prediction.
@@ -25090,8 +25112,11 @@ const getInitialSafeAreaConfig = (context) => {
25090
25112
  }
25091
25113
  // On viewports that meet the centered dialog media query breakpoints,
25092
25114
  // regular modals render as centered dialogs (not fullscreen), so they
25093
- // don't touch any screen edges and don't need safe-area insets.
25094
- if (isCenteredDialogViewport()) {
25115
+ // don't touch any screen edges and don't need safe-area insets. Also
25116
+ // applies to phone viewports when the modal declares custom --width and
25117
+ // --height; these don't touch screen edges either, so the initial
25118
+ // prediction must be zero to avoid a post-animation correction flash.
25119
+ if (isCenteredDialogViewport() || context.hasCustomDimensions) {
25095
25120
  return {
25096
25121
  top: '0px',
25097
25122
  bottom: '0px',
@@ -25403,12 +25428,10 @@ class Modal {
25403
25428
  // since the viewport may have crossed the centered-dialog breakpoint.
25404
25429
  if (!context.isSheetModal && !context.isCardModal) {
25405
25430
  this.updateSafeAreaOverrides();
25406
- // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
25407
- if (this.wrapperEl) {
25408
- this.wrapperEl.style.removeProperty('height');
25409
- this.wrapperEl.style.removeProperty('padding-bottom');
25410
- }
25411
- this.applyFullscreenSafeArea();
25431
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply.
25432
+ const { contentEl, hasFooter } = this.findContentAndFooter();
25433
+ this.clearContentSafeAreaPadding(contentEl);
25434
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
25412
25435
  }
25413
25436
  }, 50); // Debounce to avoid excessive calls during active resizing
25414
25437
  }
@@ -26155,6 +26178,11 @@ class Modal {
26155
26178
  }
26156
26179
  /**
26157
26180
  * Creates the context object for safe-area utilities.
26181
+ *
26182
+ * `hasCustomDimensions` is only set by `setInitialSafeAreaOverrides()`
26183
+ * because it is only read by `getInitialSafeAreaConfig()`. Other callers
26184
+ * (resize handler, post-animation update, fullscreen-padding apply) would
26185
+ * pay a `getComputedStyle()` cost for a value they never consult.
26158
26186
  */
26159
26187
  getSafeAreaContext() {
26160
26188
  return {
@@ -26176,7 +26204,7 @@ class Modal {
26176
26204
  * sheets to prevent header content from getting double-offset padding).
26177
26205
  */
26178
26206
  setInitialSafeAreaOverrides() {
26179
- const context = this.getSafeAreaContext();
26207
+ const context = Object.assign(Object.assign({}, this.getSafeAreaContext()), { hasCustomDimensions: hasCustomModalDimensions(this.el) });
26180
26208
  const safeAreaConfig = getInitialSafeAreaConfig(context);
26181
26209
  applySafeAreaOverrides(this.el, safeAreaConfig);
26182
26210
  // Set the internal offset property with the resolved root safe-area-top value
@@ -26216,59 +26244,85 @@ class Modal {
26216
26244
  applySafeAreaOverrides(el, safeAreaConfig);
26217
26245
  }
26218
26246
  /**
26219
- * Applies padding-bottom to fullscreen modal wrapper to prevent
26220
- * content from overlapping system navigation bar.
26247
+ * Applies safe-area-bottom scroll padding to ion-content inside
26248
+ * fullscreen modals that have no ion-footer. This prevents content
26249
+ * from being hidden behind the system navigation bar while keeping
26250
+ * the modal background edge-to-edge (no visible gap).
26221
26251
  */
26222
26252
  applyFullscreenSafeArea() {
26223
- const { wrapperEl, el } = this;
26224
- if (!wrapperEl)
26225
- return;
26226
26253
  const context = this.getSafeAreaContext();
26227
26254
  if (context.isSheetModal || context.isCardModal)
26228
26255
  return;
26229
- // Check for standard Ionic layout children (ion-content, ion-footer),
26230
- // searching one level deep for wrapped components (e.g.,
26231
- // <app-footer><ion-footer>...</ion-footer></app-footer>).
26232
- // Note: uses a manual loop instead of querySelector(':scope > ...') because
26233
- // Stencil's mock-doc (used in spec tests) does not support :scope.
26234
- let hasContent = false;
26256
+ const { contentEl, hasFooter } = this.findContentAndFooter();
26257
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
26258
+ }
26259
+ /**
26260
+ * Sets --ion-content-safe-area-padding-bottom on the given ion-content
26261
+ * when no footer is present, so ion-content's .inner-scroll includes
26262
+ * safe-area-bottom in its scroll padding. This keeps the modal background
26263
+ * edge-to-edge while ensuring content scrolls clear of the system nav bar.
26264
+ *
26265
+ * --ion-content-safe-area-padding-bottom is an internal CSS property used
26266
+ * only by this code path. It is not part of ion-content's public API and
26267
+ * should not be set by consumers. The default of 0px makes it a no-op
26268
+ * when unset, which is the expected state for ion-content used outside of
26269
+ * a fullscreen modal without a footer.
26270
+ */
26271
+ applyFullscreenSafeAreaTo(contentEl, hasFooter) {
26272
+ // Only apply for standard Ionic layouts (has ion-content but no
26273
+ // ion-footer). When a footer is present it handles its own safe-area
26274
+ // padding. Custom modals with raw HTML are developer-controlled.
26275
+ if (!contentEl || hasFooter)
26276
+ return;
26277
+ contentEl.style.setProperty('--ion-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
26278
+ }
26279
+ /**
26280
+ * Removes the internal --ion-content-safe-area-padding-bottom property
26281
+ * from an already-located ion-content. Callers do their own
26282
+ * findContentAndFooter() so they can also read hasFooter if needed.
26283
+ */
26284
+ clearContentSafeAreaPadding(contentEl) {
26285
+ if (!contentEl)
26286
+ return;
26287
+ contentEl.style.removeProperty('--ion-content-safe-area-padding-bottom');
26288
+ }
26289
+ /**
26290
+ * Finds ion-content and ion-footer among direct children and one level of
26291
+ * grandchildren (for wrapped components like <app-footer><ion-footer>).
26292
+ *
26293
+ * Intentionally does NOT use findIonContent() or querySelector() because
26294
+ * those search the full subtree and would match ion-content inside nested
26295
+ * routes/pages. We only want direct slot children (+ one wrapper level).
26296
+ *
26297
+ * Uses a manual loop instead of querySelector(':scope > ...') because
26298
+ * Stencil's mock-doc (used in spec tests) does not support :scope.
26299
+ */
26300
+ findContentAndFooter() {
26301
+ let contentEl = null;
26235
26302
  let hasFooter = false;
26236
- for (const child of Array.from(el.children)) {
26303
+ for (const child of Array.from(this.el.children)) {
26237
26304
  if (child.tagName === 'ION-CONTENT')
26238
- hasContent = true;
26305
+ contentEl = child;
26239
26306
  if (child.tagName === 'ION-FOOTER')
26240
26307
  hasFooter = true;
26241
26308
  for (const grandchild of Array.from(child.children)) {
26242
- if (grandchild.tagName === 'ION-CONTENT')
26243
- hasContent = true;
26309
+ if (grandchild.tagName === 'ION-CONTENT' && !contentEl)
26310
+ contentEl = grandchild;
26244
26311
  if (grandchild.tagName === 'ION-FOOTER')
26245
26312
  hasFooter = true;
26246
26313
  }
26247
26314
  }
26248
- // Only apply wrapper padding for standard Ionic layouts (has ion-content
26249
- // but no ion-footer). Custom modals with raw HTML are fully
26250
- // developer-controlled and should not be modified.
26251
- if (!hasContent || hasFooter)
26252
- return;
26253
- // Reduce wrapper height by safe-area and add equivalent padding so the
26254
- // total visual size stays the same but the flex content area shrinks.
26255
- // Using height + padding instead of box-sizing: border-box avoids
26256
- // breaking custom modals that set --border-width (border-box would
26257
- // include the border inside the height, changing the layout).
26258
- wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
26259
- wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
26315
+ return { contentEl, hasFooter };
26260
26316
  }
26261
26317
  /**
26262
- * Clears all safe-area overrides and padding from wrapper.
26318
+ * Clears all safe-area overrides and padding.
26263
26319
  */
26264
26320
  cleanupSafeAreaOverrides() {
26265
26321
  clearSafeAreaOverrides(this.el);
26266
26322
  // Remove internal sheet offset property
26267
26323
  this.el.style.removeProperty('--ion-modal-offset-top');
26268
- if (this.wrapperEl) {
26269
- this.wrapperEl.style.removeProperty('height');
26270
- this.wrapperEl.style.removeProperty('padding-bottom');
26271
- }
26324
+ const { contentEl } = this.findContentAndFooter();
26325
+ this.clearContentSafeAreaPadding(contentEl);
26272
26326
  }
26273
26327
  render() {
26274
26328
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
@@ -26277,20 +26331,20 @@ class Modal {
26277
26331
  const isCardModal = presentingElement !== undefined && mode === 'ios';
26278
26332
  const isHandleCycle = handleBehavior === 'cycle';
26279
26333
  const isSheetModalWithHandle = isSheetModal && showHandle;
26280
- return (hAsync(Host, Object.assign({ key: '1a53e8f87532abccc169ca4b24973a39c5f9ba16', "no-router": true,
26334
+ return (hAsync(Host, Object.assign({ key: '4bf38aa67df9a3f977163bba5423960bbafd16de', "no-router": true,
26281
26335
  // Allow the modal to be navigable when the handle is focusable
26282
26336
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
26283
26337
  zIndex: `${20000 + this.overlayIndex}`,
26284
- }, 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: 'fa8e0a436c0d458331402e1850f87af3dc97b582', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: 'f00de6027d3c8b5bc93db3b0f7a50a87628d40bb', class: "modal-shadow" }), hAsync("div", Object.assign({ key: 'ae5e33bd6c58e541edb2edbca92420ea02dd5175',
26338
+ }, 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: '866da40cc5fc8d3e36637098fb3066a5bc9f4e0f', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && hAsync("div", { key: '5a2a05514ea8592c8feb0465e504aa7c7af17963', class: "modal-shadow" }), hAsync("div", Object.assign({ key: '4d327115306451f57d190b06ab8cbb6191a6f1d7',
26285
26339
  /*
26286
26340
  role and aria-modal must be used on the
26287
26341
  same element. They must also be set inside the
26288
26342
  shadow DOM otherwise ion-button will not be highlighted
26289
26343
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
26290
26344
  */
26291
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: '141cdd8f8522331f4b764e2a4d79ec6596b1eb3a', class: "modal-handle",
26345
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (hAsync("button", { key: 'd1882835cc049232c0d957e3ba1e79676a07d179', class: "modal-handle",
26292
26346
  // Prevents the handle from receiving keyboard focus when it does not cycle
26293
- 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: '7de20298b61abee67a16d275c9ebd9a25ce7dd26', onSlotchange: this.onSlotChange }))));
26347
+ 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: '81dc58b09cf7d7022b04cd170f53113604364d5e', onSlotchange: this.onSlotChange }))));
26294
26348
  }
26295
26349
  get el() { return getElement(this); }
26296
26350
  static get watchers() { return {
@@ -31303,6 +31357,18 @@ class RadioGroup {
31303
31357
  // to the bottom of the screen
31304
31358
  ev.preventDefault();
31305
31359
  }
31360
+ // Inside a select interface, Enter commits the focused radio
31361
+ // value (matching native <select>). The !ev.repeat guard stops
31362
+ // a held Enter on the triggering ion-select from re-committing
31363
+ // once focus lands in the opened popover/modal.
31364
+ if (ev.key === 'Enter' && inSelectInterface && !ev.repeat) {
31365
+ const previousValue = this.value;
31366
+ this.value = current.value;
31367
+ if (previousValue !== this.value) {
31368
+ this.emitValueChange(ev);
31369
+ }
31370
+ ev.preventDefault();
31371
+ }
31306
31372
  }
31307
31373
  }
31308
31374
  /** @internal */
@@ -31335,7 +31401,7 @@ class RadioGroup {
31335
31401
  const { label, labelId, el, name, value } = this;
31336
31402
  const mode = getIonMode$1(this);
31337
31403
  renderHiddenInput(true, el, name, value, false);
31338
- return (hAsync(Host, { key: 'db593b3ed511e9395e3c7bfd91b787328692cd6d', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, class: mode }, this.renderHintText(), hAsync("slot", { key: 'd683b01c1ba34fe843c4b320bce4661a117472a5' })));
31404
+ return (hAsync(Host, { key: '377e4aa3a656cc84b742f9d7a7d4be65d20c69f5', role: "radiogroup", "aria-labelledby": label ? labelId : null, "aria-describedby": this.hintTextId, "aria-invalid": this.isInvalid ? 'true' : undefined, onClick: this.onClick, class: mode }, this.renderHintText(), hAsync("slot", { key: 'c3187a2497773b4f15cea3b413b036502bcec8c0' })));
31339
31405
  }
31340
31406
  get el() { return getElement(this); }
31341
31407
  static get watchers() { return {
@@ -37425,6 +37491,10 @@ const selectModalMdCss = () => `.sc-ion-select-modal-md-h{height:100%}ion-list.s
37425
37491
  class SelectModal {
37426
37492
  constructor(hostRef) {
37427
37493
  registerInstance(this, hostRef);
37494
+ // Tracks the option that received Enter-keydown so keyup only
37495
+ // dismisses when the press started on the same option. Prevents
37496
+ // Enter on the triggering ion-select from auto-dismissing.
37497
+ this.pendingEnterTarget = null;
37428
37498
  /**
37429
37499
  * The text to display on the cancel button.
37430
37500
  */
@@ -37474,15 +37544,22 @@ class SelectModal {
37474
37544
  return (hAsync("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, this.options.map((option) => (hAsync("ion-item", { lines: "none", class: Object.assign({
37475
37545
  // TODO FW-4784
37476
37546
  'item-radio-checked': option.value === checked
37477
- }, getClassMap(option.cssClass)) }, hAsync("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyUp: (ev) => {
37547
+ }, getClassMap(option.cssClass)) }, hAsync("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyDown: (ev) => {
37548
+ if (ev.key === 'Enter' && !ev.repeat) {
37549
+ this.pendingEnterTarget = ev.currentTarget;
37550
+ }
37551
+ }, onKeyUp: (ev) => {
37478
37552
  if (ev.key === ' ') {
37479
- /**
37480
- * Selecting a radio option with keyboard navigation,
37481
- * either through the Enter or Space keys, should
37482
- * dismiss the modal.
37483
- */
37553
+ // Space selects and dismisses in one press.
37484
37554
  this.closeModal();
37485
37555
  }
37556
+ else if (ev.key === 'Enter') {
37557
+ const shouldClose = this.pendingEnterTarget === ev.currentTarget;
37558
+ this.pendingEnterTarget = null;
37559
+ if (shouldClose) {
37560
+ this.closeModal();
37561
+ }
37562
+ }
37486
37563
  } }, option.text))))));
37487
37564
  }
37488
37565
  renderCheckboxOptions() {
@@ -37495,7 +37572,7 @@ class SelectModal {
37495
37572
  } }, option.text))));
37496
37573
  }
37497
37574
  render() {
37498
- return (hAsync(Host, { key: 'f8a4cd6ff23ff01eaa1bdaf3c046814e7b30b23b', class: getIonMode$1(this) }, hAsync("ion-header", { key: '9e29a7e57ad5cf332641111882f16852187ec8ba' }, hAsync("ion-toolbar", { key: 'e6af5d6eabbf4b10799fc8a0b8f91d29b12d41f5' }, this.header !== undefined && hAsync("ion-title", { key: '6056e52d15dbf307571d25e0305d67228a79237d' }, this.header), hAsync("ion-buttons", { key: 'c9aa4fb2e21a93f3a95c5a8f0ba8b7d5553c5a72', slot: "end" }, hAsync("ion-button", { key: '5ffbf512719bcb053b652fc96b1b6154d0593095', onClick: () => this.closeModal() }, this.cancelText)))), hAsync("ion-content", { key: '0ec9098798a4e6de7a83a0a7e9d10bdcd7c98a78' }, hAsync("ion-list", { key: 'd60b1700d3c2f8655951632de810900707a101f0' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
37575
+ return (hAsync(Host, { key: 'fda0bf6f93cd5ec9f3c64f88a52de849e0e140a2', class: getIonMode$1(this) }, hAsync("ion-header", { key: '27c0b17175a53db9ff159feeeb96451a3f011dab' }, hAsync("ion-toolbar", { key: '91a4155ebc317fbc9f1bb3e26a7e94754b953c9b' }, this.header !== undefined && hAsync("ion-title", { key: 'f6dae8e4e381f322cc90efefd9bb6ef81d4d2f3e' }, this.header), hAsync("ion-buttons", { key: 'e7760532fb2e7e7385ed6e62097d92d96ff20148', slot: "end" }, hAsync("ion-button", { key: '4999b6fc46cba138186546dca67b7950855e6fb7', onClick: () => this.closeModal() }, this.cancelText)))), hAsync("ion-content", { key: 'c73f80a4bc25b9061ea65cf11e5d811c1a4d8704' }, hAsync("ion-list", { key: 'b21905d15b36ad5eb45845e768918d2763cf48b1' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
37499
37576
  }
37500
37577
  get el() { return getElement(this); }
37501
37578
  static get style() { return {
@@ -37558,6 +37635,10 @@ const selectPopoverMdCss = () => `.sc-ion-select-popover-md-h ion-list.sc-ion-se
37558
37635
  class SelectPopover {
37559
37636
  constructor(hostRef) {
37560
37637
  registerInstance(this, hostRef);
37638
+ // Tracks the option that received Enter-keydown so keyup only
37639
+ // dismisses when the press started on the same option. Prevents
37640
+ // Enter on the triggering ion-select from auto-dismissing.
37641
+ this.pendingEnterTarget = null;
37561
37642
  /**
37562
37643
  * An array of options for the popover
37563
37644
  */
@@ -37633,21 +37714,28 @@ class SelectPopover {
37633
37714
  return (hAsync("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, options.map((option) => (hAsync("ion-item", { class: Object.assign({
37634
37715
  // TODO FW-4784
37635
37716
  'item-radio-checked': option.value === checked
37636
- }, getClassMap(option.cssClass)) }, hAsync("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyUp: (ev) => {
37717
+ }, getClassMap(option.cssClass)) }, hAsync("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyDown: (ev) => {
37718
+ if (ev.key === 'Enter' && !ev.repeat) {
37719
+ this.pendingEnterTarget = ev.currentTarget;
37720
+ }
37721
+ }, onKeyUp: (ev) => {
37637
37722
  if (ev.key === ' ') {
37638
- /**
37639
- * Selecting a radio option with keyboard navigation,
37640
- * either through the Enter or Space keys, should
37641
- * dismiss the popover.
37642
- */
37723
+ // Space selects and dismisses in one press.
37643
37724
  this.dismissParentPopover();
37644
37725
  }
37726
+ else if (ev.key === 'Enter') {
37727
+ const shouldDismiss = this.pendingEnterTarget === ev.currentTarget;
37728
+ this.pendingEnterTarget = null;
37729
+ if (shouldDismiss) {
37730
+ this.dismissParentPopover();
37731
+ }
37732
+ }
37645
37733
  } }, option.text))))));
37646
37734
  }
37647
37735
  render() {
37648
37736
  const { header, message, options, subHeader } = this;
37649
37737
  const hasSubHeaderOrMessage = subHeader !== undefined || message !== undefined;
37650
- return (hAsync(Host, { key: '0c9845a40d3fc392b0a7d64e2a6ed27d94bb7634', class: getIonMode$1(this) }, hAsync("ion-list", { key: '84a30f6661b0f8c00e6fa199658ed2adbcf27358' }, header !== undefined && hAsync("ion-list-header", { key: '13f5f56bbfbc06751fa516291a2da72629b60ece' }, header), hasSubHeaderOrMessage && (hAsync("ion-item", { key: '3d39d18e720e798bbde334e79e6832091c7dfb81' }, hAsync("ion-label", { key: 'd3051b0d140120b44bf5e79572f6f287e7cfb03a', class: "ion-text-wrap" }, subHeader !== undefined && hAsync("h3", { key: 'b16805956f3316f8ec703c123b76f717488e8637' }, subHeader), message !== undefined && hAsync("p", { key: '2215ac4ab4146a14e75a79192e319a8016286b5f' }, message)))), this.renderOptions(options))));
37738
+ return (hAsync(Host, { key: 'e7449a1ecfcdbf45a79f8e26a00253c4e146448a', class: getIonMode$1(this) }, hAsync("ion-list", { key: '52abdfc8668c3429a0dcefef8ddedb6647fdd894' }, header !== undefined && hAsync("ion-list-header", { key: '978e5c03728756feafcc60a0e10e6ec59bf2ae11' }, header), hasSubHeaderOrMessage && (hAsync("ion-item", { key: 'e93c44e7f07a76def16e4b11f0fb4780d84ed402' }, hAsync("ion-label", { key: 'bba1aac43b0bc7f4f00978dd8301985233f3725c', class: "ion-text-wrap" }, subHeader !== undefined && hAsync("h3", { key: 'ad96f6017cf2cc5219540bded2c4f1ca3b532de2' }, subHeader), message !== undefined && hAsync("p", { key: '3fd038921dc40c4d0c29734433984b279ccaeec3' }, message)))), this.renderOptions(options))));
37651
37739
  }
37652
37740
  get el() { return getElement(this); }
37653
37741
  static get style() { return {