voyager-ionic-core 8.8.4 → 8.8.6

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 (74) hide show
  1. package/components/ion-action-sheet.js +1 -1
  2. package/components/ion-content.js +1 -1
  3. package/components/ion-datetime.js +1 -1
  4. package/components/ion-modal.js +1 -1
  5. package/components/ion-radio-group.js +1 -1
  6. package/components/ion-segment.js +1 -1
  7. package/components/ion-select-modal.js +1 -1
  8. package/components/ion-select-popover.js +1 -1
  9. package/components/ion-select.js +1 -1
  10. package/components/p-0z8QSI5b.js +4 -0
  11. package/components/{p-ApmKVjaE.js → p-BGHGpkPX.js} +1 -1
  12. package/components/p-BlNv564p.js +4 -0
  13. package/components/p-D-cP12ZN.js +4 -0
  14. package/components/p-DZhbcvo5.js +4 -0
  15. package/components/{p-Bk2zuNWT.js → p-DvOO1fxp.js} +1 -1
  16. package/dist/cjs/ion-action-sheet.cjs.entry.js +4 -4
  17. package/dist/cjs/ion-app_8.cjs.entry.js +1 -1
  18. package/dist/cjs/ion-datetime_3.cjs.entry.js +30 -14
  19. package/dist/cjs/ion-modal.cjs.entry.js +99 -45
  20. package/dist/cjs/ion-radio_2.cjs.entry.js +13 -1
  21. package/dist/cjs/ion-segment_2.cjs.entry.js +6 -2
  22. package/dist/cjs/ion-select-modal.cjs.entry.js +18 -7
  23. package/dist/cjs/ion-select_3.cjs.entry.js +18 -7
  24. package/dist/collection/components/action-sheet/action-sheet.js +4 -4
  25. package/dist/collection/components/content/content.css +1 -1
  26. package/dist/collection/components/datetime/datetime.js +30 -14
  27. package/dist/collection/components/modal/modal.js +73 -44
  28. package/dist/collection/components/modal/safe-area-utils.js +27 -2
  29. package/dist/collection/components/radio-group/radio-group.js +13 -1
  30. package/dist/collection/components/radio-group/test/fixtures.js +2 -2
  31. package/dist/collection/components/segment/segment.js +6 -2
  32. package/dist/collection/components/select-modal/select-modal.js +18 -7
  33. package/dist/collection/components/select-modal/test/fixtures.js +4 -0
  34. package/dist/collection/components/select-popover/select-popover.js +18 -7
  35. package/dist/collection/components/select-popover/test/fixtures.js +4 -0
  36. package/dist/docs.json +1 -1
  37. package/dist/esm/ion-action-sheet.entry.js +4 -4
  38. package/dist/esm/ion-app_8.entry.js +1 -1
  39. package/dist/esm/ion-datetime_3.entry.js +30 -14
  40. package/dist/esm/ion-modal.entry.js +99 -45
  41. package/dist/esm/ion-radio_2.entry.js +13 -1
  42. package/dist/esm/ion-segment_2.entry.js +6 -2
  43. package/dist/esm/ion-select-modal.entry.js +18 -7
  44. package/dist/esm/ion-select_3.entry.js +18 -7
  45. package/dist/ionic/ionic.esm.js +1 -1
  46. package/dist/ionic/p-1ca9c36b.entry.js +4 -0
  47. package/dist/ionic/p-28a9e720.entry.js +4 -0
  48. package/dist/ionic/p-7761ef65.entry.js +4 -0
  49. package/dist/ionic/{p-4dd5e8e0.entry.js → p-8fda6a62.entry.js} +1 -1
  50. package/dist/ionic/p-a893c61c.entry.js +4 -0
  51. package/dist/ionic/{p-9eac4eb1.entry.js → p-aa812c4b.entry.js} +1 -1
  52. package/dist/ionic/p-cb27fe68.entry.js +4 -0
  53. package/dist/ionic/p-ce2edb36.entry.js +4 -0
  54. package/dist/types/components/datetime/datetime.d.ts +7 -0
  55. package/dist/types/components/modal/modal.d.ts +41 -3
  56. package/dist/types/components/modal/safe-area-utils.d.ts +16 -0
  57. package/dist/types/components/radio-group/test/fixtures.d.ts +1 -1
  58. package/dist/types/components/select-modal/select-modal.d.ts +1 -0
  59. package/dist/types/components/select-modal/test/fixtures.d.ts +1 -0
  60. package/dist/types/components/select-popover/select-popover.d.ts +1 -0
  61. package/dist/types/components/select-popover/test/fixtures.d.ts +1 -0
  62. package/hydrate/index.js +189 -81
  63. package/hydrate/index.mjs +189 -81
  64. package/package.json +1 -1
  65. package/components/p-1KVKSLu5.js +0 -4
  66. package/components/p-BI7WNErr.js +0 -4
  67. package/components/p-BTF2nRLo.js +0 -4
  68. package/components/p-EK4xUz-q.js +0 -4
  69. package/dist/ionic/p-4c67ce4c.entry.js +0 -4
  70. package/dist/ionic/p-51c11c47.entry.js +0 -4
  71. package/dist/ionic/p-5681dde4.entry.js +0 -4
  72. package/dist/ionic/p-9cdbabbb.entry.js +0 -4
  73. package/dist/ionic/p-cb78f5a0.entry.js +0 -4
  74. package/dist/ionic/p-e6c5f060.entry.js +0 -4
@@ -1648,6 +1648,12 @@ const createSheetGesture = (baseEl, backdropEl, wrapperEl, initialBreakpoint, ba
1648
1648
  const MODAL_INSET_MIN_WIDTH = 768;
1649
1649
  const MODAL_INSET_MIN_HEIGHT = 600;
1650
1650
  const EDGE_THRESHOLD = 5;
1651
+ /**
1652
+ * CSS values for `--width` / `--height` that are treated as fullscreen
1653
+ * (modal touches the corresponding screen edges). Empty string means the
1654
+ * property was not overridden. See `hasCustomModalDimensions()`.
1655
+ */
1656
+ const FULLSCREEN_SIZE_VALUES = new Set(['', '100%', '100vw', '100vh', '100dvw', '100dvh', '100svw', '100svh']);
1651
1657
  /**
1652
1658
  * Cache for resolved root safe-area-top value, invalidated once per frame.
1653
1659
  */
@@ -1696,6 +1702,22 @@ const getRootSafeAreaTop = () => {
1696
1702
  }
1697
1703
  return value;
1698
1704
  };
1705
+ /**
1706
+ * True when the modal host declares BOTH a non-fullscreen `--width` AND a
1707
+ * non-fullscreen `--height` (i.e. a centered-dialog-like modal that doesn't
1708
+ * touch any screen edge).
1709
+ *
1710
+ * The conservative "both axes" check avoids mis-zeroing safe-area for
1711
+ * partial-custom modals where the modal still touches top/bottom edges
1712
+ * (e.g. only `--width` overridden). Partial cases fall through to the
1713
+ * existing position-based post-animation correction.
1714
+ */
1715
+ const hasCustomModalDimensions = (hostEl) => {
1716
+ const styles = getComputedStyle(hostEl);
1717
+ const width = styles.getPropertyValue('--width').trim();
1718
+ const height = styles.getPropertyValue('--height').trim();
1719
+ return !FULLSCREEN_SIZE_VALUES.has(width) && !FULLSCREEN_SIZE_VALUES.has(height);
1720
+ };
1699
1721
  /**
1700
1722
  * Returns the initial safe-area configuration based on modal type.
1701
1723
  * This is called before animation starts and uses configuration-based prediction.
@@ -1730,8 +1752,11 @@ const getInitialSafeAreaConfig = (context) => {
1730
1752
  }
1731
1753
  // On viewports that meet the centered dialog media query breakpoints,
1732
1754
  // regular modals render as centered dialogs (not fullscreen), so they
1733
- // don't touch any screen edges and don't need safe-area insets.
1734
- if (isCenteredDialogViewport()) {
1755
+ // don't touch any screen edges and don't need safe-area insets. Also
1756
+ // applies to phone viewports when the modal declares custom --width and
1757
+ // --height; these don't touch screen edges either, so the initial
1758
+ // prediction must be zero to avoid a post-animation correction flash.
1759
+ if (isCenteredDialogViewport() || context.hasCustomDimensions) {
1735
1760
  return {
1736
1761
  top: '0px',
1737
1762
  bottom: '0px',
@@ -2033,12 +2058,10 @@ const Modal = class {
2033
2058
  // since the viewport may have crossed the centered-dialog breakpoint.
2034
2059
  if (!context.isSheetModal && !context.isCardModal) {
2035
2060
  this.updateSafeAreaOverrides();
2036
- // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
2037
- if (this.wrapperEl) {
2038
- this.wrapperEl.style.removeProperty('height');
2039
- this.wrapperEl.style.removeProperty('padding-bottom');
2040
- }
2041
- this.applyFullscreenSafeArea();
2061
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply.
2062
+ const { contentEl, hasFooter } = this.findContentAndFooter();
2063
+ this.clearContentSafeAreaPadding(contentEl);
2064
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
2042
2065
  }
2043
2066
  }, 50); // Debounce to avoid excessive calls during active resizing
2044
2067
  }
@@ -2785,6 +2808,11 @@ const Modal = class {
2785
2808
  }
2786
2809
  /**
2787
2810
  * Creates the context object for safe-area utilities.
2811
+ *
2812
+ * `hasCustomDimensions` is only set by `setInitialSafeAreaOverrides()`
2813
+ * because it is only read by `getInitialSafeAreaConfig()`. Other callers
2814
+ * (resize handler, post-animation update, fullscreen-padding apply) would
2815
+ * pay a `getComputedStyle()` cost for a value they never consult.
2788
2816
  */
2789
2817
  getSafeAreaContext() {
2790
2818
  return {
@@ -2806,7 +2834,7 @@ const Modal = class {
2806
2834
  * sheets to prevent header content from getting double-offset padding).
2807
2835
  */
2808
2836
  setInitialSafeAreaOverrides() {
2809
- const context = this.getSafeAreaContext();
2837
+ const context = Object.assign(Object.assign({}, this.getSafeAreaContext()), { hasCustomDimensions: hasCustomModalDimensions(this.el) });
2810
2838
  const safeAreaConfig = getInitialSafeAreaConfig(context);
2811
2839
  applySafeAreaOverrides(this.el, safeAreaConfig);
2812
2840
  // Set the internal offset property with the resolved root safe-area-top value
@@ -2846,59 +2874,85 @@ const Modal = class {
2846
2874
  applySafeAreaOverrides(el, safeAreaConfig);
2847
2875
  }
2848
2876
  /**
2849
- * Applies padding-bottom to fullscreen modal wrapper to prevent
2850
- * content from overlapping system navigation bar.
2877
+ * Applies safe-area-bottom scroll padding to ion-content inside
2878
+ * fullscreen modals that have no ion-footer. This prevents content
2879
+ * from being hidden behind the system navigation bar while keeping
2880
+ * the modal background edge-to-edge (no visible gap).
2851
2881
  */
2852
2882
  applyFullscreenSafeArea() {
2853
- const { wrapperEl, el } = this;
2854
- if (!wrapperEl)
2855
- return;
2856
2883
  const context = this.getSafeAreaContext();
2857
2884
  if (context.isSheetModal || context.isCardModal)
2858
2885
  return;
2859
- // Check for standard Ionic layout children (ion-content, ion-footer),
2860
- // searching one level deep for wrapped components (e.g.,
2861
- // <app-footer><ion-footer>...</ion-footer></app-footer>).
2862
- // Note: uses a manual loop instead of querySelector(':scope > ...') because
2863
- // Stencil's mock-doc (used in spec tests) does not support :scope.
2864
- let hasContent = false;
2886
+ const { contentEl, hasFooter } = this.findContentAndFooter();
2887
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
2888
+ }
2889
+ /**
2890
+ * Sets --ion-content-safe-area-padding-bottom on the given ion-content
2891
+ * when no footer is present, so ion-content's .inner-scroll includes
2892
+ * safe-area-bottom in its scroll padding. This keeps the modal background
2893
+ * edge-to-edge while ensuring content scrolls clear of the system nav bar.
2894
+ *
2895
+ * --ion-content-safe-area-padding-bottom is an internal CSS property used
2896
+ * only by this code path. It is not part of ion-content's public API and
2897
+ * should not be set by consumers. The default of 0px makes it a no-op
2898
+ * when unset, which is the expected state for ion-content used outside of
2899
+ * a fullscreen modal without a footer.
2900
+ */
2901
+ applyFullscreenSafeAreaTo(contentEl, hasFooter) {
2902
+ // Only apply for standard Ionic layouts (has ion-content but no
2903
+ // ion-footer). When a footer is present it handles its own safe-area
2904
+ // padding. Custom modals with raw HTML are developer-controlled.
2905
+ if (!contentEl || hasFooter)
2906
+ return;
2907
+ contentEl.style.setProperty('--ion-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2908
+ }
2909
+ /**
2910
+ * Removes the internal --ion-content-safe-area-padding-bottom property
2911
+ * from an already-located ion-content. Callers do their own
2912
+ * findContentAndFooter() so they can also read hasFooter if needed.
2913
+ */
2914
+ clearContentSafeAreaPadding(contentEl) {
2915
+ if (!contentEl)
2916
+ return;
2917
+ contentEl.style.removeProperty('--ion-content-safe-area-padding-bottom');
2918
+ }
2919
+ /**
2920
+ * Finds ion-content and ion-footer among direct children and one level of
2921
+ * grandchildren (for wrapped components like <app-footer><ion-footer>).
2922
+ *
2923
+ * Intentionally does NOT use findIonContent() or querySelector() because
2924
+ * those search the full subtree and would match ion-content inside nested
2925
+ * routes/pages. We only want direct slot children (+ one wrapper level).
2926
+ *
2927
+ * Uses a manual loop instead of querySelector(':scope > ...') because
2928
+ * Stencil's mock-doc (used in spec tests) does not support :scope.
2929
+ */
2930
+ findContentAndFooter() {
2931
+ let contentEl = null;
2865
2932
  let hasFooter = false;
2866
- for (const child of Array.from(el.children)) {
2933
+ for (const child of Array.from(this.el.children)) {
2867
2934
  if (child.tagName === 'ION-CONTENT')
2868
- hasContent = true;
2935
+ contentEl = child;
2869
2936
  if (child.tagName === 'ION-FOOTER')
2870
2937
  hasFooter = true;
2871
2938
  for (const grandchild of Array.from(child.children)) {
2872
- if (grandchild.tagName === 'ION-CONTENT')
2873
- hasContent = true;
2939
+ if (grandchild.tagName === 'ION-CONTENT' && !contentEl)
2940
+ contentEl = grandchild;
2874
2941
  if (grandchild.tagName === 'ION-FOOTER')
2875
2942
  hasFooter = true;
2876
2943
  }
2877
2944
  }
2878
- // Only apply wrapper padding for standard Ionic layouts (has ion-content
2879
- // but no ion-footer). Custom modals with raw HTML are fully
2880
- // developer-controlled and should not be modified.
2881
- if (!hasContent || hasFooter)
2882
- return;
2883
- // Reduce wrapper height by safe-area and add equivalent padding so the
2884
- // total visual size stays the same but the flex content area shrinks.
2885
- // Using height + padding instead of box-sizing: border-box avoids
2886
- // breaking custom modals that set --border-width (border-box would
2887
- // include the border inside the height, changing the layout).
2888
- wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
2889
- wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
2945
+ return { contentEl, hasFooter };
2890
2946
  }
2891
2947
  /**
2892
- * Clears all safe-area overrides and padding from wrapper.
2948
+ * Clears all safe-area overrides and padding.
2893
2949
  */
2894
2950
  cleanupSafeAreaOverrides() {
2895
2951
  clearSafeAreaOverrides(this.el);
2896
2952
  // Remove internal sheet offset property
2897
2953
  this.el.style.removeProperty('--ion-modal-offset-top');
2898
- if (this.wrapperEl) {
2899
- this.wrapperEl.style.removeProperty('height');
2900
- this.wrapperEl.style.removeProperty('padding-bottom');
2901
- }
2954
+ const { contentEl } = this.findContentAndFooter();
2955
+ this.clearContentSafeAreaPadding(contentEl);
2902
2956
  }
2903
2957
  render() {
2904
2958
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
@@ -2907,20 +2961,20 @@ const Modal = class {
2907
2961
  const isCardModal = presentingElement !== undefined && mode === 'ios';
2908
2962
  const isHandleCycle = handleBehavior === 'cycle';
2909
2963
  const isSheetModalWithHandle = isSheetModal && showHandle;
2910
- return (index$3.h(index$3.Host, Object.assign({ key: '1a53e8f87532abccc169ca4b24973a39c5f9ba16', "no-router": true,
2964
+ return (index$3.h(index$3.Host, Object.assign({ key: '4bf38aa67df9a3f977163bba5423960bbafd16de', "no-router": true,
2911
2965
  // Allow the modal to be navigable when the handle is focusable
2912
2966
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
2913
2967
  zIndex: `${20000 + this.overlayIndex}`,
2914
- }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: 'fa8e0a436c0d458331402e1850f87af3dc97b582', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: 'f00de6027d3c8b5bc93db3b0f7a50a87628d40bb', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: 'ae5e33bd6c58e541edb2edbca92420ea02dd5175',
2968
+ }, class: Object.assign({ [mode]: true, ['modal-default']: !isCardModal && !isSheetModal, [`modal-card`]: isCardModal, [`modal-sheet`]: isSheetModal, [`modal-no-expand-scroll`]: isSheetModal && !expandToScroll, 'overlay-hidden': true, [overlays.FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false }, theme.getClassMap(this.cssClass)), onIonBackdropTap: this.onBackdropTap, onIonModalDidPresent: this.onLifecycle, onIonModalWillPresent: this.onLifecycle, onIonModalWillDismiss: this.onLifecycle, onIonModalDidDismiss: this.onLifecycle, onFocus: this.onModalFocus }), index$3.h("ion-backdrop", { key: '866da40cc5fc8d3e36637098fb3066a5bc9f4e0f', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && index$3.h("div", { key: '5a2a05514ea8592c8feb0465e504aa7c7af17963', class: "modal-shadow" }), index$3.h("div", Object.assign({ key: '4d327115306451f57d190b06ab8cbb6191a6f1d7',
2915
2969
  /*
2916
2970
  role and aria-modal must be used on the
2917
2971
  same element. They must also be set inside the
2918
2972
  shadow DOM otherwise ion-button will not be highlighted
2919
2973
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
2920
2974
  */
2921
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: '141cdd8f8522331f4b764e2a4d79ec6596b1eb3a', class: "modal-handle",
2975
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (index$3.h("button", { key: 'd1882835cc049232c0d957e3ba1e79676a07d179', class: "modal-handle",
2922
2976
  // Prevents the handle from receiving keyboard focus when it does not cycle
2923
- 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) })), index$3.h("slot", { key: '7de20298b61abee67a16d275c9ebd9a25ce7dd26', onSlotchange: this.onSlotChange }))));
2977
+ 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) })), index$3.h("slot", { key: '81dc58b09cf7d7022b04cd170f53113604364d5e', onSlotchange: this.onSlotChange }))));
2924
2978
  }
2925
2979
  get el() { return index$3.getElement(this); }
2926
2980
  static get watchers() { return {
@@ -375,6 +375,18 @@ const RadioGroup = class {
375
375
  // to the bottom of the screen
376
376
  ev.preventDefault();
377
377
  }
378
+ // Inside a select interface, Enter commits the focused radio
379
+ // value (matching native <select>). The !ev.repeat guard stops
380
+ // a held Enter on the triggering ion-select from re-committing
381
+ // once focus lands in the opened popover/modal.
382
+ if (ev.key === 'Enter' && inSelectInterface && !ev.repeat) {
383
+ const previousValue = this.value;
384
+ this.value = current.value;
385
+ if (previousValue !== this.value) {
386
+ this.emitValueChange(ev);
387
+ }
388
+ ev.preventDefault();
389
+ }
378
390
  }
379
391
  }
380
392
  /** @internal */
@@ -407,7 +419,7 @@ const RadioGroup = class {
407
419
  const { label, labelId, el, name, value } = this;
408
420
  const mode = ionicGlobal.getIonMode(this);
409
421
  helpers.renderHiddenInput(true, el, name, value, false);
410
- return (index.h(index.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(), index.h("slot", { key: 'd683b01c1ba34fe843c4b320bce4661a117472a5' })));
422
+ return (index.h(index.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(), index.h("slot", { key: 'c3187a2497773b4f15cea3b413b036502bcec8c0' })));
411
423
  }
412
424
  get el() { return index.getElement(this); }
413
425
  static get watchers() { return {
@@ -323,6 +323,7 @@ const Segment = class {
323
323
  return segmentContent === null || segmentContent === void 0 ? void 0 : segmentContent.closest('ion-segment-view');
324
324
  }
325
325
  handleSegmentViewScroll(ev) {
326
+ var _a;
326
327
  const { scrollRatio, isManualScroll } = ev.detail;
327
328
  if (!isManualScroll) {
328
329
  return;
@@ -339,6 +340,9 @@ const Segment = class {
339
340
  const index = buttons.findIndex((button) => button.value === this.value);
340
341
  const current = buttons[index];
341
342
  const nextIndex = Math.round(scrollRatio * (buttons.length - 1));
343
+ if ((_a = buttons[nextIndex]) === null || _a === void 0 ? void 0 : _a.disabled) {
344
+ return;
345
+ }
342
346
  if (this.lastNextIndex === undefined || this.lastNextIndex !== nextIndex) {
343
347
  this.lastNextIndex = nextIndex;
344
348
  this.triggerScrollOnValueChange = false;
@@ -549,14 +553,14 @@ const Segment = class {
549
553
  }
550
554
  render() {
551
555
  const mode = ionicGlobal.getIonMode(this);
552
- return (index.h(index.Host, { key: '725cc37b25c539fa5e3ae8d90530ae33ededc3de', role: "tablist", onClick: this.onClick, class: theme.createColorClasses(this.color, {
556
+ return (index.h(index.Host, { key: 'eda6b7b88b7967b55cf9098c59b655b348a42224', role: "tablist", onClick: this.onClick, class: theme.createColorClasses(this.color, {
553
557
  [mode]: true,
554
558
  'in-toolbar': theme.hostContext('ion-toolbar', this.el),
555
559
  'in-toolbar-color': theme.hostContext('ion-toolbar[color]', this.el),
556
560
  'segment-activated': this.activated,
557
561
  'segment-disabled': this.disabled,
558
562
  'segment-scrollable': this.scrollable,
559
- }) }, index.h("slot", { key: 'c51cf7ea50325866a9367d214e12bc3754870335', onSlotchange: this.onSlottedItemsChange })));
563
+ }) }, index.h("slot", { key: 'fdb451f235ce59c5bb50c61a13c69160ece2d5df', onSlotchange: this.onSlottedItemsChange })));
560
564
  }
561
565
  get el() { return index.getElement(this); }
562
566
  static get watchers() { return {
@@ -22,6 +22,10 @@ const selectModalMdCss = () => `.sc-ion-select-modal-md-h{height:100%}ion-list.s
22
22
  const SelectModal = class {
23
23
  constructor(hostRef) {
24
24
  index.registerInstance(this, hostRef);
25
+ // Tracks the option that received Enter-keydown so keyup only
26
+ // dismisses when the press started on the same option. Prevents
27
+ // Enter on the triggering ion-select from auto-dismissing.
28
+ this.pendingEnterTarget = null;
25
29
  /**
26
30
  * The text to display on the cancel button.
27
31
  */
@@ -71,15 +75,22 @@ const SelectModal = class {
71
75
  return (index.h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, this.options.map((option) => (index.h("ion-item", { lines: "none", class: Object.assign({
72
76
  // TODO FW-4784
73
77
  'item-radio-checked': option.value === checked
74
- }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyUp: (ev) => {
78
+ }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyDown: (ev) => {
79
+ if (ev.key === 'Enter' && !ev.repeat) {
80
+ this.pendingEnterTarget = ev.currentTarget;
81
+ }
82
+ }, onKeyUp: (ev) => {
75
83
  if (ev.key === ' ') {
76
- /**
77
- * Selecting a radio option with keyboard navigation,
78
- * either through the Enter or Space keys, should
79
- * dismiss the modal.
80
- */
84
+ // Space selects and dismisses in one press.
81
85
  this.closeModal();
82
86
  }
87
+ else if (ev.key === 'Enter') {
88
+ const shouldClose = this.pendingEnterTarget === ev.currentTarget;
89
+ this.pendingEnterTarget = null;
90
+ if (shouldClose) {
91
+ this.closeModal();
92
+ }
93
+ }
83
94
  } }, option.text))))));
84
95
  }
85
96
  renderCheckboxOptions() {
@@ -94,7 +105,7 @@ const SelectModal = class {
94
105
  } }, option.text))));
95
106
  }
96
107
  render() {
97
- return (index.h(index.Host, { key: 'f8a4cd6ff23ff01eaa1bdaf3c046814e7b30b23b', class: ionicGlobal.getIonMode(this) }, index.h("ion-header", { key: '9e29a7e57ad5cf332641111882f16852187ec8ba' }, index.h("ion-toolbar", { key: 'e6af5d6eabbf4b10799fc8a0b8f91d29b12d41f5' }, this.header !== undefined && index.h("ion-title", { key: '6056e52d15dbf307571d25e0305d67228a79237d' }, this.header), index.h("ion-buttons", { key: 'c9aa4fb2e21a93f3a95c5a8f0ba8b7d5553c5a72', slot: "end" }, index.h("ion-button", { key: '5ffbf512719bcb053b652fc96b1b6154d0593095', onClick: () => this.closeModal() }, this.cancelText)))), index.h("ion-content", { key: '0ec9098798a4e6de7a83a0a7e9d10bdcd7c98a78' }, index.h("ion-list", { key: 'd60b1700d3c2f8655951632de810900707a101f0' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
108
+ return (index.h(index.Host, { key: 'fda0bf6f93cd5ec9f3c64f88a52de849e0e140a2', class: ionicGlobal.getIonMode(this) }, index.h("ion-header", { key: '27c0b17175a53db9ff159feeeb96451a3f011dab' }, index.h("ion-toolbar", { key: '91a4155ebc317fbc9f1bb3e26a7e94754b953c9b' }, this.header !== undefined && index.h("ion-title", { key: 'f6dae8e4e381f322cc90efefd9bb6ef81d4d2f3e' }, this.header), index.h("ion-buttons", { key: 'e7760532fb2e7e7385ed6e62097d92d96ff20148', slot: "end" }, index.h("ion-button", { key: '4999b6fc46cba138186546dca67b7950855e6fb7', onClick: () => this.closeModal() }, this.cancelText)))), index.h("ion-content", { key: 'c73f80a4bc25b9061ea65cf11e5d811c1a4d8704' }, index.h("ion-list", { key: 'b21905d15b36ad5eb45845e768918d2763cf48b1' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
98
109
  }
99
110
  get el() { return index.getElement(this); }
100
111
  };
@@ -892,6 +892,10 @@ const selectPopoverMdCss = () => `.sc-ion-select-popover-md-h ion-list.sc-ion-se
892
892
  const SelectPopover = class {
893
893
  constructor(hostRef) {
894
894
  index.registerInstance(this, hostRef);
895
+ // Tracks the option that received Enter-keydown so keyup only
896
+ // dismisses when the press started on the same option. Prevents
897
+ // Enter on the triggering ion-select from auto-dismissing.
898
+ this.pendingEnterTarget = null;
895
899
  /**
896
900
  * An array of options for the popover
897
901
  */
@@ -969,21 +973,28 @@ const SelectPopover = class {
969
973
  return (index.h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, options.map((option) => (index.h("ion-item", { class: Object.assign({
970
974
  // TODO FW-4784
971
975
  'item-radio-checked': option.value === checked
972
- }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyUp: (ev) => {
976
+ }, theme.getClassMap(option.cssClass)) }, index.h("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyDown: (ev) => {
977
+ if (ev.key === 'Enter' && !ev.repeat) {
978
+ this.pendingEnterTarget = ev.currentTarget;
979
+ }
980
+ }, onKeyUp: (ev) => {
973
981
  if (ev.key === ' ') {
974
- /**
975
- * Selecting a radio option with keyboard navigation,
976
- * either through the Enter or Space keys, should
977
- * dismiss the popover.
978
- */
982
+ // Space selects and dismisses in one press.
979
983
  this.dismissParentPopover();
980
984
  }
985
+ else if (ev.key === 'Enter') {
986
+ const shouldDismiss = this.pendingEnterTarget === ev.currentTarget;
987
+ this.pendingEnterTarget = null;
988
+ if (shouldDismiss) {
989
+ this.dismissParentPopover();
990
+ }
991
+ }
981
992
  } }, option.text))))));
982
993
  }
983
994
  render() {
984
995
  const { header, message, options, subHeader } = this;
985
996
  const hasSubHeaderOrMessage = subHeader !== undefined || message !== undefined;
986
- return (index.h(index.Host, { key: '0c9845a40d3fc392b0a7d64e2a6ed27d94bb7634', class: ionicGlobal.getIonMode(this) }, index.h("ion-list", { key: '84a30f6661b0f8c00e6fa199658ed2adbcf27358' }, header !== undefined && index.h("ion-list-header", { key: '13f5f56bbfbc06751fa516291a2da72629b60ece' }, header), hasSubHeaderOrMessage && (index.h("ion-item", { key: '3d39d18e720e798bbde334e79e6832091c7dfb81' }, index.h("ion-label", { key: 'd3051b0d140120b44bf5e79572f6f287e7cfb03a', class: "ion-text-wrap" }, subHeader !== undefined && index.h("h3", { key: 'b16805956f3316f8ec703c123b76f717488e8637' }, subHeader), message !== undefined && index.h("p", { key: '2215ac4ab4146a14e75a79192e319a8016286b5f' }, message)))), this.renderOptions(options))));
997
+ return (index.h(index.Host, { key: 'e7449a1ecfcdbf45a79f8e26a00253c4e146448a', class: ionicGlobal.getIonMode(this) }, index.h("ion-list", { key: '52abdfc8668c3429a0dcefef8ddedb6647fdd894' }, header !== undefined && index.h("ion-list-header", { key: '978e5c03728756feafcc60a0e10e6ec59bf2ae11' }, header), hasSubHeaderOrMessage && (index.h("ion-item", { key: 'e93c44e7f07a76def16e4b11f0fb4780d84ed402' }, index.h("ion-label", { key: 'bba1aac43b0bc7f4f00978dd8301985233f3725c', class: "ion-text-wrap" }, subHeader !== undefined && index.h("h3", { key: 'ad96f6017cf2cc5219540bded2c4f1ca3b532de2' }, subHeader), message !== undefined && index.h("p", { key: '3fd038921dc40c4d0c29734433984b279ccaeec3' }, message)))), this.renderOptions(options))));
987
998
  }
988
999
  get el() { return index.getElement(this); }
989
1000
  };
@@ -372,7 +372,7 @@ export class ActionSheet {
372
372
  if (isRadio) {
373
373
  htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
374
374
  }
375
- return (h("button", Object.assign({}, htmlAttrs, { role: isRadio ? 'radio' : undefined, type: "button", id: buttonId, class: Object.assign(Object.assign({}, buttonClass(b)), { 'action-sheet-selected': isActiveRadio }), onClick: () => {
375
+ return (h("button", Object.assign({}, htmlAttrs, { role: isRadio ? 'radio' : undefined, type: "button", id: buttonId, class: Object.assign(Object.assign({}, buttonClass(b)), (isRadio && { 'action-sheet-selected': isActiveRadio })), onClick: () => {
376
376
  if (isRadio) {
377
377
  this.selectRadioButton(b);
378
378
  }
@@ -387,12 +387,12 @@ export class ActionSheet {
387
387
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
388
388
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
389
389
  const headerID = `action-sheet-${overlayIndex}-header`;
390
- return (h(Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
390
+ return (h(Host, Object.assign({ key: '48b63b870f2816b4cad3c606f3d9956854cee79a', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
391
391
  zIndex: `${20000 + this.overlayIndex}`,
392
- }, class: Object.assign(Object.assign({ [mode]: true }, getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), h("ion-backdrop", { key: '521ede659f747864f6c974e09016436eceb7158c', tappable: this.backdropDismiss }), h("div", { key: '7a7946fc434bc444f16a70638f5e948c69d33fcd', tabindex: "0", "aria-hidden": "true" }), h("div", { key: 'bcff39a580489dbafa255842e57aa8602c6d0f18', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, h("div", { key: '84bba13ce14261f0f0daa3f9c77648c9e7f36e0e', class: "action-sheet-container" }, h("div", { key: 'd9c8ac404fd6719a7adf8cb36549f67616f9a0c4', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (h("div", { key: '180433a8ad03ef5c54728a1a8f34715b6921d658', id: headerID, class: {
392
+ }, class: Object.assign(Object.assign({ [mode]: true }, getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), h("ion-backdrop", { key: '41dd5781f139d26b3feea33ab387451aeafacd51', tappable: this.backdropDismiss }), h("div", { key: 'f797c2657782e4e83adf90d2d796108e857a1fc0', tabindex: "0", "aria-hidden": "true" }), h("div", { key: '8e74209321fc5e8712e3e293c91a6fa036ea45ab', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, h("div", { key: 'c811860be2eed0b6c73fc2cc5a59cb94db2e8912', class: "action-sheet-container" }, h("div", { key: 'd36ee14b70d73eb5cd69e0c57bc5cc31daf86ab3', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (h("div", { key: '922695de191edc86451eed2552acc4f54993ea52', id: headerID, class: {
393
393
  'action-sheet-title': true,
394
394
  'action-sheet-has-sub-title': this.subHeader !== undefined,
395
- } }, header, this.subHeader && h("div", { key: '7138e79e61b1a8f42bc5a9175c57fa2f15d7ec5a', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (h("div", { key: 'b617c722f5b8028d73ed34b69310f312c65f34a7', class: "action-sheet-group action-sheet-group-cancel" }, h("button", Object.assign({ key: 'd0dd876fc48815df3710413c201c0b445a8e16c0' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), h("span", { key: 'e7b960157cc6fc5fe92a12090b2be55e8ae072e4', class: "action-sheet-button-inner" }, cancelButton.icon && (h("ion-icon", { key: '05498ffc60cab911dbff0ecbc6168dea59ada9a5', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && h("ion-ripple-effect", { key: '3d401346cea301be4ca03671f7370f6f4b0b6bde' })))))), h("div", { key: '971f3c5fcc07f36c28eb469a47ec0290c692e139', tabindex: "0", "aria-hidden": "true" })));
395
+ } }, header, this.subHeader && h("div", { key: 'a119d4a93668e829f7106f11cbbdd437310f3e80', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (h("div", { key: '1d27f36d09bedd2e4c61fea9dd9d05f8f9271aef', class: "action-sheet-group action-sheet-group-cancel" }, h("button", Object.assign({ key: 'ed13658176c54cc45808a54a0331c297430d9bc6' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), h("span", { key: '5208e3e3535b775a443cef6fb1decd790db69b0c', class: "action-sheet-button-inner" }, cancelButton.icon && (h("ion-icon", { key: 'b7ba489e1ee50524a5b4af670eef787844a16286', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && h("ion-ripple-effect", { key: 'b3f88898114855853259f0001def26b4bd6e4a98' })))))), h("div", { key: '0dfd0fcdc633bf565990eaa06679608e749cf8f9', tabindex: "0", "aria-hidden": "true" })));
396
396
  }
397
397
  static get is() { return "ion-action-sheet"; }
398
398
  static get encapsulation() { return "scoped"; }
@@ -116,7 +116,7 @@
116
116
  -webkit-padding-end: var(--padding-end);
117
117
  padding-inline-end: var(--padding-end);
118
118
  padding-top: calc(var(--padding-top) + var(--offset-top));
119
- padding-bottom: calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom));
119
+ padding-bottom: calc(var(--padding-bottom) + var(--keyboard-offset) + var(--offset-bottom) + var(--ion-content-safe-area-padding-bottom, 0px));
120
120
  position: absolute;
121
121
  color: var(--color);
122
122
  box-sizing: border-box;
@@ -55,6 +55,12 @@ import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from "./ut
55
55
  export class Datetime {
56
56
  constructor() {
57
57
  this.inputId = `ion-dt-${datetimeIds++}`;
58
+ /**
59
+ * Set true only by `visibleCallback`. Lets `hiddenCallback` ignore the
60
+ * synthetic "not intersecting" entry IntersectionObserver fires on
61
+ * `observe()` when the host mounts offscreen.
62
+ */
63
+ this.hasBeenIntersecting = false;
58
64
  this.prevPresentation = null;
59
65
  this.showMonthAndYear = false;
60
66
  this.activeParts = [];
@@ -617,7 +623,21 @@ export class Datetime {
617
623
  if (rect.width === 0 || rect.height === 0) {
618
624
  return;
619
625
  }
626
+ this.markReady();
627
+ };
628
+ this.markReady = () => {
629
+ if (this.el.classList.contains('datetime-ready')) {
630
+ return;
631
+ }
620
632
  this.initializeListeners();
633
+ /**
634
+ * TODO FW-2793: Datetime needs a frame to ensure that it
635
+ * can properly scroll contents into view. As a result
636
+ * we hide the scrollable content until after that frame
637
+ * so users do not see the content quickly shifting. The downside
638
+ * is that the content will pop into view a frame after. Maybe there
639
+ * is a better way to handle this?
640
+ */
621
641
  writeTask(() => {
622
642
  this.el.classList.add('datetime-ready');
623
643
  });
@@ -910,6 +930,7 @@ export class Datetime {
910
930
  this.clearFocusVisible = undefined;
911
931
  }
912
932
  this.loadTimeoutCleanup();
933
+ this.hasBeenIntersecting = false;
913
934
  }
914
935
  initializeListeners() {
915
936
  this.initializeCalendarListener();
@@ -929,18 +950,8 @@ export class Datetime {
929
950
  if (!ev.isIntersecting) {
930
951
  return;
931
952
  }
932
- this.initializeListeners();
933
- /**
934
- * TODO FW-2793: Datetime needs a frame to ensure that it
935
- * can properly scroll contents into view. As a result
936
- * we hide the scrollable content until after that frame
937
- * so users do not see the content quickly shifting. The downside
938
- * is that the content will pop into view a frame after. Maybe there
939
- * is a better way to handle this?
940
- */
941
- writeTask(() => {
942
- this.el.classList.add('datetime-ready');
943
- });
953
+ this.hasBeenIntersecting = true;
954
+ this.markReady();
944
955
  };
945
956
  const visibleIO = new IntersectionObserver(visibleCallback, { threshold: 0.01, root: el });
946
957
  /**
@@ -976,6 +987,11 @@ export class Datetime {
976
987
  if (ev.isIntersecting) {
977
988
  return;
978
989
  }
990
+ // Ignore the initial "not intersecting" entry IntersectionObserver fires on observe().
991
+ if (!this.hasBeenIntersecting) {
992
+ return;
993
+ }
994
+ this.hasBeenIntersecting = false;
979
995
  this.destroyInteractionListeners();
980
996
  /**
981
997
  * When datetime is hidden, we need to make sure that
@@ -1719,7 +1735,7 @@ export class Datetime {
1719
1735
  const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
1720
1736
  const hasWheelVariant = hasDatePresentation && preferWheel;
1721
1737
  renderHiddenInput(true, el, name, formatValue(value), disabled);
1722
- return (h(Host, { key: '59e0811aa273e88dfb8e4b703e6824088a457380', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1738
+ return (h(Host, { key: '323c8c2327088f00934b8c93c3306538cb9b5677', "aria-disabled": disabled ? 'true' : null, onFocus: this.onFocus, onBlur: this.onBlur, class: Object.assign({}, createColorClasses(color, {
1723
1739
  [mode]: true,
1724
1740
  ['datetime-readonly']: readonly,
1725
1741
  ['datetime-disabled']: disabled,
@@ -1729,7 +1745,7 @@ export class Datetime {
1729
1745
  [`datetime-size-${size}`]: true,
1730
1746
  [`datetime-prefer-wheel`]: hasWheelVariant,
1731
1747
  [`datetime-grid`]: isGridStyle,
1732
- })) }, h("div", { key: '3753ff3dde3085070916c3de83687a219a49e553', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1748
+ })) }, h("div", { key: '1e0855c8909bc3f1e48a21ad68159fa782060691', class: "intersection-tracker", ref: (el) => (this.intersectionTrackerRef = el) }), this.renderDatetime(mode)));
1733
1749
  }
1734
1750
  static get is() { return "ion-datetime"; }
1735
1751
  static get encapsulation() { return "shadow"; }