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
@@ -21,7 +21,7 @@ import { mdEnterAnimation } from "./animations/md.enter";
21
21
  import { mdLeaveAnimation } from "./animations/md.leave";
22
22
  import { createSheetGesture } from "./gestures/sheet";
23
23
  import { createSwipeToCloseGesture, SwipeToCloseDefaults } from "./gestures/swipe-to-close";
24
- import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, } from "./safe-area-utils";
24
+ import { getInitialSafeAreaConfig, getPositionBasedSafeAreaConfig, applySafeAreaOverrides, clearSafeAreaOverrides, getRootSafeAreaTop, hasCustomModalDimensions, } from "./safe-area-utils";
25
25
  import { setCardStatusBarDark, setCardStatusBarDefault } from "./utils";
26
26
  // TODO(FW-2832): types
27
27
  /**
@@ -251,12 +251,10 @@ export class Modal {
251
251
  // since the viewport may have crossed the centered-dialog breakpoint.
252
252
  if (!context.isSheetModal && !context.isCardModal) {
253
253
  this.updateSafeAreaOverrides();
254
- // Re-evaluate fullscreen safe-area padding: clear first, then re-apply
255
- if (this.wrapperEl) {
256
- this.wrapperEl.style.removeProperty('height');
257
- this.wrapperEl.style.removeProperty('padding-bottom');
258
- }
259
- this.applyFullscreenSafeArea();
254
+ // Re-evaluate fullscreen safe-area padding: clear first, then re-apply.
255
+ const { contentEl, hasFooter } = this.findContentAndFooter();
256
+ this.clearContentSafeAreaPadding(contentEl);
257
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
260
258
  }
261
259
  }, 50); // Debounce to avoid excessive calls during active resizing
262
260
  }
@@ -1011,6 +1009,11 @@ export class Modal {
1011
1009
  }
1012
1010
  /**
1013
1011
  * Creates the context object for safe-area utilities.
1012
+ *
1013
+ * `hasCustomDimensions` is only set by `setInitialSafeAreaOverrides()`
1014
+ * because it is only read by `getInitialSafeAreaConfig()`. Other callers
1015
+ * (resize handler, post-animation update, fullscreen-padding apply) would
1016
+ * pay a `getComputedStyle()` cost for a value they never consult.
1014
1017
  */
1015
1018
  getSafeAreaContext() {
1016
1019
  return {
@@ -1032,7 +1035,7 @@ export class Modal {
1032
1035
  * sheets to prevent header content from getting double-offset padding).
1033
1036
  */
1034
1037
  setInitialSafeAreaOverrides() {
1035
- const context = this.getSafeAreaContext();
1038
+ const context = Object.assign(Object.assign({}, this.getSafeAreaContext()), { hasCustomDimensions: hasCustomModalDimensions(this.el) });
1036
1039
  const safeAreaConfig = getInitialSafeAreaConfig(context);
1037
1040
  applySafeAreaOverrides(this.el, safeAreaConfig);
1038
1041
  // Set the internal offset property with the resolved root safe-area-top value
@@ -1072,59 +1075,85 @@ export class Modal {
1072
1075
  applySafeAreaOverrides(el, safeAreaConfig);
1073
1076
  }
1074
1077
  /**
1075
- * Applies padding-bottom to fullscreen modal wrapper to prevent
1076
- * content from overlapping system navigation bar.
1078
+ * Applies safe-area-bottom scroll padding to ion-content inside
1079
+ * fullscreen modals that have no ion-footer. This prevents content
1080
+ * from being hidden behind the system navigation bar while keeping
1081
+ * the modal background edge-to-edge (no visible gap).
1077
1082
  */
1078
1083
  applyFullscreenSafeArea() {
1079
- const { wrapperEl, el } = this;
1080
- if (!wrapperEl)
1081
- return;
1082
1084
  const context = this.getSafeAreaContext();
1083
1085
  if (context.isSheetModal || context.isCardModal)
1084
1086
  return;
1085
- // Check for standard Ionic layout children (ion-content, ion-footer),
1086
- // searching one level deep for wrapped components (e.g.,
1087
- // <app-footer><ion-footer>...</ion-footer></app-footer>).
1088
- // Note: uses a manual loop instead of querySelector(':scope > ...') because
1089
- // Stencil's mock-doc (used in spec tests) does not support :scope.
1090
- let hasContent = false;
1087
+ const { contentEl, hasFooter } = this.findContentAndFooter();
1088
+ this.applyFullscreenSafeAreaTo(contentEl, hasFooter);
1089
+ }
1090
+ /**
1091
+ * Sets --ion-content-safe-area-padding-bottom on the given ion-content
1092
+ * when no footer is present, so ion-content's .inner-scroll includes
1093
+ * safe-area-bottom in its scroll padding. This keeps the modal background
1094
+ * edge-to-edge while ensuring content scrolls clear of the system nav bar.
1095
+ *
1096
+ * --ion-content-safe-area-padding-bottom is an internal CSS property used
1097
+ * only by this code path. It is not part of ion-content's public API and
1098
+ * should not be set by consumers. The default of 0px makes it a no-op
1099
+ * when unset, which is the expected state for ion-content used outside of
1100
+ * a fullscreen modal without a footer.
1101
+ */
1102
+ applyFullscreenSafeAreaTo(contentEl, hasFooter) {
1103
+ // Only apply for standard Ionic layouts (has ion-content but no
1104
+ // ion-footer). When a footer is present it handles its own safe-area
1105
+ // padding. Custom modals with raw HTML are developer-controlled.
1106
+ if (!contentEl || hasFooter)
1107
+ return;
1108
+ contentEl.style.setProperty('--ion-content-safe-area-padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
1109
+ }
1110
+ /**
1111
+ * Removes the internal --ion-content-safe-area-padding-bottom property
1112
+ * from an already-located ion-content. Callers do their own
1113
+ * findContentAndFooter() so they can also read hasFooter if needed.
1114
+ */
1115
+ clearContentSafeAreaPadding(contentEl) {
1116
+ if (!contentEl)
1117
+ return;
1118
+ contentEl.style.removeProperty('--ion-content-safe-area-padding-bottom');
1119
+ }
1120
+ /**
1121
+ * Finds ion-content and ion-footer among direct children and one level of
1122
+ * grandchildren (for wrapped components like <app-footer><ion-footer>).
1123
+ *
1124
+ * Intentionally does NOT use findIonContent() or querySelector() because
1125
+ * those search the full subtree and would match ion-content inside nested
1126
+ * routes/pages. We only want direct slot children (+ one wrapper level).
1127
+ *
1128
+ * Uses a manual loop instead of querySelector(':scope > ...') because
1129
+ * Stencil's mock-doc (used in spec tests) does not support :scope.
1130
+ */
1131
+ findContentAndFooter() {
1132
+ let contentEl = null;
1091
1133
  let hasFooter = false;
1092
- for (const child of Array.from(el.children)) {
1134
+ for (const child of Array.from(this.el.children)) {
1093
1135
  if (child.tagName === 'ION-CONTENT')
1094
- hasContent = true;
1136
+ contentEl = child;
1095
1137
  if (child.tagName === 'ION-FOOTER')
1096
1138
  hasFooter = true;
1097
1139
  for (const grandchild of Array.from(child.children)) {
1098
- if (grandchild.tagName === 'ION-CONTENT')
1099
- hasContent = true;
1140
+ if (grandchild.tagName === 'ION-CONTENT' && !contentEl)
1141
+ contentEl = grandchild;
1100
1142
  if (grandchild.tagName === 'ION-FOOTER')
1101
1143
  hasFooter = true;
1102
1144
  }
1103
1145
  }
1104
- // Only apply wrapper padding for standard Ionic layouts (has ion-content
1105
- // but no ion-footer). Custom modals with raw HTML are fully
1106
- // developer-controlled and should not be modified.
1107
- if (!hasContent || hasFooter)
1108
- return;
1109
- // Reduce wrapper height by safe-area and add equivalent padding so the
1110
- // total visual size stays the same but the flex content area shrinks.
1111
- // Using height + padding instead of box-sizing: border-box avoids
1112
- // breaking custom modals that set --border-width (border-box would
1113
- // include the border inside the height, changing the layout).
1114
- wrapperEl.style.setProperty('height', 'calc(var(--height) - var(--ion-safe-area-bottom, 0px))');
1115
- wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
1146
+ return { contentEl, hasFooter };
1116
1147
  }
1117
1148
  /**
1118
- * Clears all safe-area overrides and padding from wrapper.
1149
+ * Clears all safe-area overrides and padding.
1119
1150
  */
1120
1151
  cleanupSafeAreaOverrides() {
1121
1152
  clearSafeAreaOverrides(this.el);
1122
1153
  // Remove internal sheet offset property
1123
1154
  this.el.style.removeProperty('--ion-modal-offset-top');
1124
- if (this.wrapperEl) {
1125
- this.wrapperEl.style.removeProperty('height');
1126
- this.wrapperEl.style.removeProperty('padding-bottom');
1127
- }
1155
+ const { contentEl } = this.findContentAndFooter();
1156
+ this.clearContentSafeAreaPadding(contentEl);
1128
1157
  }
1129
1158
  render() {
1130
1159
  const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap, expandToScroll, } = this;
@@ -1133,20 +1162,20 @@ export class Modal {
1133
1162
  const isCardModal = presentingElement !== undefined && mode === 'ios';
1134
1163
  const isHandleCycle = handleBehavior === 'cycle';
1135
1164
  const isSheetModalWithHandle = isSheetModal && showHandle;
1136
- return (h(Host, Object.assign({ key: '1a53e8f87532abccc169ca4b24973a39c5f9ba16', "no-router": true,
1165
+ return (h(Host, Object.assign({ key: '4bf38aa67df9a3f977163bba5423960bbafd16de', "no-router": true,
1137
1166
  // Allow the modal to be navigable when the handle is focusable
1138
1167
  tabIndex: isHandleCycle && isSheetModalWithHandle ? 0 : -1 }, htmlAttributes, { style: {
1139
1168
  zIndex: `${20000 + this.overlayIndex}`,
1140
- }, 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 }), h("ion-backdrop", { key: 'fa8e0a436c0d458331402e1850f87af3dc97b582', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: 'f00de6027d3c8b5bc93db3b0f7a50a87628d40bb', class: "modal-shadow" }), h("div", Object.assign({ key: 'ae5e33bd6c58e541edb2edbca92420ea02dd5175',
1169
+ }, 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 }), h("ion-backdrop", { key: '866da40cc5fc8d3e36637098fb3066a5bc9f4e0f', ref: (el) => (this.backdropEl = el), visible: this.showBackdrop, tappable: this.backdropDismiss, part: "backdrop" }), mode === 'ios' && h("div", { key: '5a2a05514ea8592c8feb0465e504aa7c7af17963', class: "modal-shadow" }), h("div", Object.assign({ key: '4d327115306451f57d190b06ab8cbb6191a6f1d7',
1141
1170
  /*
1142
1171
  role and aria-modal must be used on the
1143
1172
  same element. They must also be set inside the
1144
1173
  shadow DOM otherwise ion-button will not be highlighted
1145
1174
  when using VoiceOver: https://bugs.webkit.org/show_bug.cgi?id=247134
1146
1175
  */
1147
- role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: '141cdd8f8522331f4b764e2a4d79ec6596b1eb3a', class: "modal-handle",
1176
+ role: "dialog" }, inheritedAttributes, { "aria-modal": "true", class: "modal-wrapper ion-overlay-wrapper", part: "content", ref: (el) => (this.wrapperEl = el) }), showHandle && (h("button", { key: 'd1882835cc049232c0d957e3ba1e79676a07d179', class: "modal-handle",
1148
1177
  // Prevents the handle from receiving keyboard focus when it does not cycle
1149
- 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) })), h("slot", { key: '7de20298b61abee67a16d275c9ebd9a25ce7dd26', onSlotchange: this.onSlotChange }))));
1178
+ 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) })), h("slot", { key: '81dc58b09cf7d7022b04cd170f53113604364d5e', onSlotchange: this.onSlotChange }))));
1150
1179
  }
1151
1180
  static get is() { return "ion-modal"; }
1152
1181
  static get encapsulation() { return "shadow"; }
@@ -15,6 +15,12 @@ import { raf } from "../../utils/helpers";
15
15
  const MODAL_INSET_MIN_WIDTH = 768;
16
16
  const MODAL_INSET_MIN_HEIGHT = 600;
17
17
  const EDGE_THRESHOLD = 5;
18
+ /**
19
+ * CSS values for `--width` / `--height` that are treated as fullscreen
20
+ * (modal touches the corresponding screen edges). Empty string means the
21
+ * property was not overridden. See `hasCustomModalDimensions()`.
22
+ */
23
+ const FULLSCREEN_SIZE_VALUES = new Set(['', '100%', '100vw', '100vh', '100dvw', '100dvh', '100svw', '100svh']);
18
24
  /**
19
25
  * Cache for resolved root safe-area-top value, invalidated once per frame.
20
26
  */
@@ -63,6 +69,22 @@ export const getRootSafeAreaTop = () => {
63
69
  }
64
70
  return value;
65
71
  };
72
+ /**
73
+ * True when the modal host declares BOTH a non-fullscreen `--width` AND a
74
+ * non-fullscreen `--height` (i.e. a centered-dialog-like modal that doesn't
75
+ * touch any screen edge).
76
+ *
77
+ * The conservative "both axes" check avoids mis-zeroing safe-area for
78
+ * partial-custom modals where the modal still touches top/bottom edges
79
+ * (e.g. only `--width` overridden). Partial cases fall through to the
80
+ * existing position-based post-animation correction.
81
+ */
82
+ export const hasCustomModalDimensions = (hostEl) => {
83
+ const styles = getComputedStyle(hostEl);
84
+ const width = styles.getPropertyValue('--width').trim();
85
+ const height = styles.getPropertyValue('--height').trim();
86
+ return !FULLSCREEN_SIZE_VALUES.has(width) && !FULLSCREEN_SIZE_VALUES.has(height);
87
+ };
66
88
  /**
67
89
  * Returns the initial safe-area configuration based on modal type.
68
90
  * This is called before animation starts and uses configuration-based prediction.
@@ -97,8 +119,11 @@ export const getInitialSafeAreaConfig = (context) => {
97
119
  }
98
120
  // On viewports that meet the centered dialog media query breakpoints,
99
121
  // regular modals render as centered dialogs (not fullscreen), so they
100
- // don't touch any screen edges and don't need safe-area insets.
101
- if (isCenteredDialogViewport()) {
122
+ // don't touch any screen edges and don't need safe-area insets. Also
123
+ // applies to phone viewports when the modal declares custom --width and
124
+ // --height; these don't touch screen edges either, so the initial
125
+ // prediction must be zero to avoid a post-animation correction flash.
126
+ if (isCenteredDialogViewport() || context.hasCustomDimensions) {
102
127
  return {
103
128
  top: '0px',
104
129
  bottom: '0px',
@@ -206,6 +206,18 @@ export class RadioGroup {
206
206
  // to the bottom of the screen
207
207
  ev.preventDefault();
208
208
  }
209
+ // Inside a select interface, Enter commits the focused radio
210
+ // value (matching native <select>). The !ev.repeat guard stops
211
+ // a held Enter on the triggering ion-select from re-committing
212
+ // once focus lands in the opened popover/modal.
213
+ if (ev.key === 'Enter' && inSelectInterface && !ev.repeat) {
214
+ const previousValue = this.value;
215
+ this.value = current.value;
216
+ if (previousValue !== this.value) {
217
+ this.emitValueChange(ev);
218
+ }
219
+ ev.preventDefault();
220
+ }
209
221
  }
210
222
  }
211
223
  /** @internal */
@@ -238,7 +250,7 @@ export class RadioGroup {
238
250
  const { label, labelId, el, name, value } = this;
239
251
  const mode = getIonMode(this);
240
252
  renderHiddenInput(true, el, name, value, false);
241
- return (h(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(), h("slot", { key: 'd683b01c1ba34fe843c4b320bce4661a117472a5' })));
253
+ return (h(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(), h("slot", { key: 'c3187a2497773b4f15cea3b413b036502bcec8c0' })));
242
254
  }
243
255
  static get is() { return "ion-radio-group"; }
244
256
  static get originalStyleUrls() {
@@ -6,12 +6,12 @@ export class RadioFixture {
6
6
  constructor(page) {
7
7
  this.page = page;
8
8
  }
9
- async checkRadio(method, selector = 'ion-radio') {
9
+ async checkRadio(method, selector = 'ion-radio', key = 'Space') {
10
10
  const { page } = this;
11
11
  const radio = (this.radio = page.locator(selector));
12
12
  if (method === 'keyboard') {
13
13
  await radio.focus();
14
- await page.keyboard.press('Space');
14
+ await page.keyboard.press(key);
15
15
  }
16
16
  else {
17
17
  await radio.click();
@@ -7,6 +7,10 @@ import { safeCall } from "../../utils/overlays";
7
7
  import { getClassMap } from "../../utils/theme";
8
8
  export class SelectModal {
9
9
  constructor() {
10
+ // Tracks the option that received Enter-keydown so keyup only
11
+ // dismisses when the press started on the same option. Prevents
12
+ // Enter on the triggering ion-select from auto-dismissing.
13
+ this.pendingEnterTarget = null;
10
14
  /**
11
15
  * The text to display on the cancel button.
12
16
  */
@@ -56,15 +60,22 @@ export class SelectModal {
56
60
  return (h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, this.options.map((option) => (h("ion-item", { lines: "none", class: Object.assign({
57
61
  // TODO FW-4784
58
62
  'item-radio-checked': option.value === checked
59
- }, getClassMap(option.cssClass)) }, h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyUp: (ev) => {
63
+ }, getClassMap(option.cssClass)) }, h("ion-radio", { value: option.value, disabled: option.disabled, justify: "start", labelPlacement: "end", onClick: () => this.closeModal(), onKeyDown: (ev) => {
64
+ if (ev.key === 'Enter' && !ev.repeat) {
65
+ this.pendingEnterTarget = ev.currentTarget;
66
+ }
67
+ }, onKeyUp: (ev) => {
60
68
  if (ev.key === ' ') {
61
- /**
62
- * Selecting a radio option with keyboard navigation,
63
- * either through the Enter or Space keys, should
64
- * dismiss the modal.
65
- */
69
+ // Space selects and dismisses in one press.
66
70
  this.closeModal();
67
71
  }
72
+ else if (ev.key === 'Enter') {
73
+ const shouldClose = this.pendingEnterTarget === ev.currentTarget;
74
+ this.pendingEnterTarget = null;
75
+ if (shouldClose) {
76
+ this.closeModal();
77
+ }
78
+ }
68
79
  } }, option.text))))));
69
80
  }
70
81
  renderCheckboxOptions() {
@@ -79,7 +90,7 @@ export class SelectModal {
79
90
  } }, option.text))));
80
91
  }
81
92
  render() {
82
- return (h(Host, { key: 'f8a4cd6ff23ff01eaa1bdaf3c046814e7b30b23b', class: getIonMode(this) }, h("ion-header", { key: '9e29a7e57ad5cf332641111882f16852187ec8ba' }, h("ion-toolbar", { key: 'e6af5d6eabbf4b10799fc8a0b8f91d29b12d41f5' }, this.header !== undefined && h("ion-title", { key: '6056e52d15dbf307571d25e0305d67228a79237d' }, this.header), h("ion-buttons", { key: 'c9aa4fb2e21a93f3a95c5a8f0ba8b7d5553c5a72', slot: "end" }, h("ion-button", { key: '5ffbf512719bcb053b652fc96b1b6154d0593095', onClick: () => this.closeModal() }, this.cancelText)))), h("ion-content", { key: '0ec9098798a4e6de7a83a0a7e9d10bdcd7c98a78' }, h("ion-list", { key: 'd60b1700d3c2f8655951632de810900707a101f0' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
93
+ return (h(Host, { key: 'fda0bf6f93cd5ec9f3c64f88a52de849e0e140a2', class: getIonMode(this) }, h("ion-header", { key: '27c0b17175a53db9ff159feeeb96451a3f011dab' }, h("ion-toolbar", { key: '91a4155ebc317fbc9f1bb3e26a7e94754b953c9b' }, this.header !== undefined && h("ion-title", { key: 'f6dae8e4e381f322cc90efefd9bb6ef81d4d2f3e' }, this.header), h("ion-buttons", { key: 'e7760532fb2e7e7385ed6e62097d92d96ff20148', slot: "end" }, h("ion-button", { key: '4999b6fc46cba138186546dca67b7950855e6fb7', onClick: () => this.closeModal() }, this.cancelText)))), h("ion-content", { key: 'c73f80a4bc25b9061ea65cf11e5d811c1a4d8704' }, h("ion-list", { key: 'b21905d15b36ad5eb45845e768918d2763cf48b1' }, this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()))));
83
94
  }
84
95
  static get is() { return "ion-select-modal"; }
85
96
  static get encapsulation() { return "scoped"; }
@@ -39,6 +39,10 @@ export class SelectModalPage {
39
39
  const option = this.getOption(value);
40
40
  await option.press('Space');
41
41
  }
42
+ async pressEnterOnOption(value) {
43
+ const option = this.getOption(value);
44
+ await option.press('Enter');
45
+ }
42
46
  getOption(value) {
43
47
  const { multiple, selectModal } = this;
44
48
  const selector = multiple ? 'ion-checkbox' : 'ion-radio';
@@ -10,6 +10,10 @@ import { getIonMode } from "../../global/ionic-global";
10
10
  */
11
11
  export class SelectPopover {
12
12
  constructor() {
13
+ // Tracks the option that received Enter-keydown so keyup only
14
+ // dismisses when the press started on the same option. Prevents
15
+ // Enter on the triggering ion-select from auto-dismissing.
16
+ this.pendingEnterTarget = null;
13
17
  /**
14
18
  * An array of options for the popover
15
19
  */
@@ -87,21 +91,28 @@ export class SelectPopover {
87
91
  return (h("ion-radio-group", { value: checked, onIonChange: (ev) => this.callOptionHandler(ev) }, options.map((option) => (h("ion-item", { class: Object.assign({
88
92
  // TODO FW-4784
89
93
  'item-radio-checked': option.value === checked
90
- }, getClassMap(option.cssClass)) }, h("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyUp: (ev) => {
94
+ }, getClassMap(option.cssClass)) }, h("ion-radio", { value: option.value, disabled: option.disabled, onClick: () => this.dismissParentPopover(), onKeyDown: (ev) => {
95
+ if (ev.key === 'Enter' && !ev.repeat) {
96
+ this.pendingEnterTarget = ev.currentTarget;
97
+ }
98
+ }, onKeyUp: (ev) => {
91
99
  if (ev.key === ' ') {
92
- /**
93
- * Selecting a radio option with keyboard navigation,
94
- * either through the Enter or Space keys, should
95
- * dismiss the popover.
96
- */
100
+ // Space selects and dismisses in one press.
97
101
  this.dismissParentPopover();
98
102
  }
103
+ else if (ev.key === 'Enter') {
104
+ const shouldDismiss = this.pendingEnterTarget === ev.currentTarget;
105
+ this.pendingEnterTarget = null;
106
+ if (shouldDismiss) {
107
+ this.dismissParentPopover();
108
+ }
109
+ }
99
110
  } }, option.text))))));
100
111
  }
101
112
  render() {
102
113
  const { header, message, options, subHeader } = this;
103
114
  const hasSubHeaderOrMessage = subHeader !== undefined || message !== undefined;
104
- return (h(Host, { key: '0c9845a40d3fc392b0a7d64e2a6ed27d94bb7634', class: getIonMode(this) }, h("ion-list", { key: '84a30f6661b0f8c00e6fa199658ed2adbcf27358' }, header !== undefined && h("ion-list-header", { key: '13f5f56bbfbc06751fa516291a2da72629b60ece' }, header), hasSubHeaderOrMessage && (h("ion-item", { key: '3d39d18e720e798bbde334e79e6832091c7dfb81' }, h("ion-label", { key: 'd3051b0d140120b44bf5e79572f6f287e7cfb03a', class: "ion-text-wrap" }, subHeader !== undefined && h("h3", { key: 'b16805956f3316f8ec703c123b76f717488e8637' }, subHeader), message !== undefined && h("p", { key: '2215ac4ab4146a14e75a79192e319a8016286b5f' }, message)))), this.renderOptions(options))));
115
+ return (h(Host, { key: 'e7449a1ecfcdbf45a79f8e26a00253c4e146448a', class: getIonMode(this) }, h("ion-list", { key: '52abdfc8668c3429a0dcefef8ddedb6647fdd894' }, header !== undefined && h("ion-list-header", { key: '978e5c03728756feafcc60a0e10e6ec59bf2ae11' }, header), hasSubHeaderOrMessage && (h("ion-item", { key: 'e93c44e7f07a76def16e4b11f0fb4780d84ed402' }, h("ion-label", { key: 'bba1aac43b0bc7f4f00978dd8301985233f3725c', class: "ion-text-wrap" }, subHeader !== undefined && h("h3", { key: 'ad96f6017cf2cc5219540bded2c4f1ca3b532de2' }, subHeader), message !== undefined && h("p", { key: '3fd038921dc40c4d0c29734433984b279ccaeec3' }, message)))), this.renderOptions(options))));
105
116
  }
106
117
  static get is() { return "ion-select-popover"; }
107
118
  static get encapsulation() { return "scoped"; }
@@ -39,6 +39,10 @@ export class SelectPopoverPage {
39
39
  const option = this.getOption(value);
40
40
  await option.press('Space');
41
41
  }
42
+ async pressEnterOnOption(value) {
43
+ const option = this.getOption(value);
44
+ await option.press('Enter');
45
+ }
42
46
  getOption(value) {
43
47
  const { multiple, selectPopover } = this;
44
48
  const selector = multiple ? 'ion-checkbox' : 'ion-radio';
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2026-04-17T14:19:57",
2
+ "timestamp": "2026-05-01T03:59:54",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.43.0",
@@ -471,7 +471,7 @@ const ActionSheet = class {
471
471
  if (isRadio) {
472
472
  htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
473
473
  }
474
- 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: () => {
474
+ 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: () => {
475
475
  if (isRadio) {
476
476
  this.selectRadioButton(b);
477
477
  }
@@ -486,12 +486,12 @@ const ActionSheet = class {
486
486
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
487
487
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
488
488
  const headerID = `action-sheet-${overlayIndex}-header`;
489
- return (h(Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
489
+ return (h(Host, Object.assign({ key: 'a56ee2ab59c763036140dbd10306a708c26e3c17', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
490
490
  zIndex: `${20000 + this.overlayIndex}`,
491
- }, 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: {
491
+ }, 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: 'c32eb4281fd6348c7d3989a3f509c211263048e6', tappable: this.backdropDismiss }), h("div", { key: '7f0123114a876fc7cfff3cfb564aded4a7017797', tabindex: "0", "aria-hidden": "true" }), h("div", { key: '645b1d5fde39a8907f21983d66e6ecb7a99aa05d', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, h("div", { key: 'a78fb02848462d1a4f9356ac4fa1c43a2e5d90e4', class: "action-sheet-container" }, h("div", { key: '5e846f53e067b211b985d6e1512b72b9d7c1a3aa', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (h("div", { key: 'a90a0e096e1b2fa78b9adb9253c0a517f16e62cb', id: headerID, class: {
492
492
  'action-sheet-title': true,
493
493
  'action-sheet-has-sub-title': this.subHeader !== undefined,
494
- } }, 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" })));
494
+ } }, header, this.subHeader && h("div", { key: '40f00b12341625c548546de1885b9c9d93bc169c', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (h("div", { key: 'ef6974cb63089623df08087274b82745443cee8c', class: "action-sheet-group action-sheet-group-cancel" }, h("button", Object.assign({ key: 'b02911a6491d60f9dcb5da7d942392a9e96552c1' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), h("span", { key: '1187433e676eda55e52b5ae328a8e68bba22deb6', class: "action-sheet-button-inner" }, cancelButton.icon && (h("ion-icon", { key: '079ab2a6bd40b996950053617f1c1c8207ecb1f1', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && h("ion-ripple-effect", { key: '3bc473add8ac299f202f8c359d26708872c02f52' })))))), h("div", { key: '9b1ae7b4e3649e9b85632f0d65627ca81499e68d', tabindex: "0", "aria-hidden": "true" })));
495
495
  }
496
496
  get el() { return getElement(this); }
497
497
  static get watchers() { return {
@@ -157,7 +157,7 @@ Buttons.style = {
157
157
  md: buttonsMdCss()
158
158
  };
159
159
 
160
- 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)}`;
160
+ 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)}`;
161
161
 
162
162
  const Content = class {
163
163
  constructor(hostRef) {