@ionic/core 8.7.12-nightly.20251208 → 8.7.12-nightly.20251209

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.
@@ -20,6 +20,7 @@ export class ActionSheet {
20
20
  this.delegateController = createDelegateController(this);
21
21
  this.lockController = createLockController();
22
22
  this.triggerController = createTriggerController();
23
+ this.hasRadioButtons = false;
23
24
  this.presented = false;
24
25
  /** @internal */
25
26
  this.hasController = false;
@@ -64,6 +65,19 @@ export class ActionSheet {
64
65
  }
65
66
  };
66
67
  }
68
+ buttonsChanged() {
69
+ const radioButtons = this.getRadioButtons();
70
+ this.hasRadioButtons = radioButtons.length > 0;
71
+ // Initialize activeRadioId when buttons change
72
+ if (this.hasRadioButtons) {
73
+ const checkedButton = radioButtons.find((b) => { var _a; return ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a['aria-checked']) === 'true'; });
74
+ if (checkedButton) {
75
+ const allButtons = this.getButtons();
76
+ const checkedIndex = allButtons.indexOf(checkedButton);
77
+ this.activeRadioId = this.getButtonId(checkedButton, checkedIndex);
78
+ }
79
+ }
80
+ }
67
81
  onIsOpenChange(newValue, oldValue) {
68
82
  if (newValue === true && oldValue === false) {
69
83
  this.present();
@@ -144,11 +158,122 @@ export class ActionSheet {
144
158
  }
145
159
  return true;
146
160
  }
161
+ /**
162
+ * Get all buttons regardless of role.
163
+ */
147
164
  getButtons() {
148
165
  return this.buttons.map((b) => {
149
166
  return typeof b === 'string' ? { text: b } : b;
150
167
  });
151
168
  }
169
+ /**
170
+ * Get all radio buttons (buttons with role="radio").
171
+ */
172
+ getRadioButtons() {
173
+ return this.getButtons().filter((b) => {
174
+ var _a;
175
+ const role = (_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role;
176
+ return role === 'radio' && !isCancel(role);
177
+ });
178
+ }
179
+ /**
180
+ * Handle radio button selection and update aria-checked state.
181
+ *
182
+ * @param button The radio button that was selected.
183
+ */
184
+ selectRadioButton(button) {
185
+ const buttonId = this.getButtonId(button);
186
+ // Set the active radio ID (this will trigger a re-render and update aria-checked)
187
+ this.activeRadioId = buttonId;
188
+ }
189
+ /**
190
+ * Get or generate an ID for a button.
191
+ *
192
+ * @param button The button for which to get the ID.
193
+ * @param index Optional index of the button in the buttons array.
194
+ * @returns The ID of the button.
195
+ */
196
+ getButtonId(button, index) {
197
+ if (button.id) {
198
+ return button.id;
199
+ }
200
+ const allButtons = this.getButtons();
201
+ const buttonIndex = index !== undefined ? index : allButtons.indexOf(button);
202
+ return `action-sheet-button-${this.overlayIndex}-${buttonIndex}`;
203
+ }
204
+ /**
205
+ * When the action sheet has radio buttons, we want to follow the
206
+ * keyboard navigation pattern for radio groups:
207
+ * - Arrow Down/Right: Move to the next radio button (wrap to first if at end)
208
+ * - Arrow Up/Left: Move to the previous radio button (wrap to last if at start)
209
+ * - Space/Enter: Select the focused radio button and trigger its handler
210
+ */
211
+ onKeydown(ev) {
212
+ // Only handle keyboard navigation if we have radio buttons
213
+ if (!this.hasRadioButtons || !this.presented) {
214
+ return;
215
+ }
216
+ const target = ev.target;
217
+ // Ignore if the target element is not within the action sheet or not a radio button
218
+ if (!this.el.contains(target) ||
219
+ !target.classList.contains('action-sheet-button') ||
220
+ target.getAttribute('role') !== 'radio') {
221
+ return;
222
+ }
223
+ // Get all radio button elements and filter out disabled ones
224
+ const radios = Array.from(this.el.querySelectorAll('.action-sheet-button[role="radio"]')).filter((el) => !el.disabled);
225
+ const currentIndex = radios.findIndex((radio) => radio.id === target.id);
226
+ if (currentIndex === -1) {
227
+ return;
228
+ }
229
+ const allButtons = this.getButtons();
230
+ const radioButtons = this.getRadioButtons();
231
+ /**
232
+ * Build a map of button element IDs to their ActionSheetButton
233
+ * config objects.
234
+ * This allows us to quickly look up which button config corresponds
235
+ * to a DOM element when handling keyboard navigation
236
+ * (e.g., whenuser presses Space/Enter or arrow keys).
237
+ * The key is the ID that was set on the DOM element during render,
238
+ * and the value is the ActionSheetButton config that contains the
239
+ * handler and other properties.
240
+ */
241
+ const buttonIdMap = new Map();
242
+ radioButtons.forEach((b) => {
243
+ const allIndex = allButtons.indexOf(b);
244
+ const buttonId = this.getButtonId(b, allIndex);
245
+ buttonIdMap.set(buttonId, b);
246
+ });
247
+ let nextEl;
248
+ if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
249
+ ev.preventDefault();
250
+ ev.stopPropagation();
251
+ nextEl = currentIndex === radios.length - 1 ? radios[0] : radios[currentIndex + 1];
252
+ }
253
+ else if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
254
+ ev.preventDefault();
255
+ ev.stopPropagation();
256
+ nextEl = currentIndex === 0 ? radios[radios.length - 1] : radios[currentIndex - 1];
257
+ }
258
+ else if (ev.key === ' ' || ev.key === 'Enter') {
259
+ ev.preventDefault();
260
+ ev.stopPropagation();
261
+ const button = buttonIdMap.get(target.id);
262
+ if (button) {
263
+ this.selectRadioButton(button);
264
+ this.buttonClick(button);
265
+ }
266
+ return;
267
+ }
268
+ // Focus the next radio button
269
+ if (nextEl) {
270
+ const button = buttonIdMap.get(nextEl.id);
271
+ if (button) {
272
+ this.selectRadioButton(button);
273
+ nextEl.focus();
274
+ }
275
+ }
276
+ }
152
277
  connectedCallback() {
153
278
  prepareOverlay(this.el);
154
279
  this.triggerChanged();
@@ -165,6 +290,8 @@ export class ActionSheet {
165
290
  if (!((_a = this.htmlAttributes) === null || _a === void 0 ? void 0 : _a.id)) {
166
291
  setOverlayId(this.el);
167
292
  }
293
+ // Initialize activeRadioId for radio buttons
294
+ this.buttonsChanged();
168
295
  }
169
296
  componentDidLoad() {
170
297
  /**
@@ -202,19 +329,70 @@ export class ActionSheet {
202
329
  */
203
330
  this.triggerChanged();
204
331
  }
332
+ renderActionSheetButtons(filteredButtons) {
333
+ const mode = getIonMode(this);
334
+ const { activeRadioId } = this;
335
+ return filteredButtons.map((b, index) => {
336
+ var _a;
337
+ const isRadio = ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role) === 'radio';
338
+ const buttonId = this.getButtonId(b, index);
339
+ const radioButtons = this.getRadioButtons();
340
+ const isActiveRadio = isRadio && buttonId === activeRadioId;
341
+ const isFirstRadio = isRadio && b === radioButtons[0];
342
+ // For radio buttons, set tabindex: 0 for the active one, -1 for others
343
+ // For non-radio buttons, use default tabindex (undefined, which means 0)
344
+ /**
345
+ * For radio buttons, set tabindex based on activeRadioId
346
+ * - If the button is the active radio, tabindex is 0
347
+ * - If no radio is active, the first radio button should have tabindex 0
348
+ * - All other radio buttons have tabindex -1
349
+ * For non-radio buttons, use default tabindex (undefined, which means 0)
350
+ */
351
+ let tabIndex;
352
+ if (isRadio) {
353
+ // Focus on the active radio button
354
+ if (isActiveRadio) {
355
+ tabIndex = 0;
356
+ }
357
+ else if (!activeRadioId && isFirstRadio) {
358
+ // No active radio, first radio gets focus
359
+ tabIndex = 0;
360
+ }
361
+ else {
362
+ // All other radios are not focusable
363
+ tabIndex = -1;
364
+ }
365
+ }
366
+ else {
367
+ tabIndex = undefined;
368
+ }
369
+ // For radio buttons, set aria-checked based on activeRadioId
370
+ // Otherwise, use the value from htmlAttributes if provided
371
+ const htmlAttrs = Object.assign({}, b.htmlAttributes);
372
+ if (isRadio) {
373
+ htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
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: () => {
376
+ if (isRadio) {
377
+ this.selectRadioButton(b);
378
+ }
379
+ this.buttonClick(b);
380
+ }, disabled: b.disabled, tabIndex: tabIndex }), h("span", { class: "action-sheet-button-inner" }, b.icon && h("ion-icon", { icon: b.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" }), b.text), mode === 'md' && h("ion-ripple-effect", null)));
381
+ });
382
+ }
205
383
  render() {
206
- const { header, htmlAttributes, overlayIndex } = this;
384
+ const { header, htmlAttributes, overlayIndex, hasRadioButtons } = this;
207
385
  const mode = getIonMode(this);
208
386
  const allButtons = this.getButtons();
209
387
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
210
388
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
211
389
  const headerID = `action-sheet-${overlayIndex}-header`;
212
- return (h(Host, Object.assign({ key: '9fef156b2a1f09ca4a6c1fe1f37c374139bde03c', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
390
+ return (h(Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
213
391
  zIndex: `${20000 + this.overlayIndex}`,
214
- }, 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: '81cf3f7d19864e041813987b46d2d115b8466819', tappable: this.backdropDismiss }), h("div", { key: '791c6a976683646fc306a42c15c5078b6f06a45f', tabindex: "0", "aria-hidden": "true" }), h("div", { key: 'a350b489ef7852eab9dc2227ce6d92da27dd9bf9', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, h("div", { key: '69ba51ee13510c1a411d87cb4845b11b7302a36f', class: "action-sheet-container" }, h("div", { key: 'bded15b8306c36591e526f0f99e1eeabcbab3915', class: "action-sheet-group", ref: (el) => (this.groupEl = el) }, header !== undefined && (h("div", { key: '06b5147c0f6d9180fe8f12e75c9b4a0310226adc', 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: '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: {
215
393
  'action-sheet-title': true,
216
394
  'action-sheet-has-sub-title': this.subHeader !== undefined,
217
- } }, header, this.subHeader && h("div", { key: '54874362a75c679aba803bf4f8768f5404d2dd28', class: "action-sheet-sub-title" }, this.subHeader))), buttons.map((b) => (h("button", Object.assign({}, b.htmlAttributes, { type: "button", id: b.id, class: buttonClass(b), onClick: () => this.buttonClick(b), disabled: b.disabled }), h("span", { class: "action-sheet-button-inner" }, b.icon && h("ion-icon", { icon: b.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" }), b.text), mode === 'md' && h("ion-ripple-effect", null))))), cancelButton && (h("div", { key: '67b0de298eb424f3dea846a841b7a06d70e3930d', class: "action-sheet-group action-sheet-group-cancel" }, h("button", Object.assign({ key: 'e7e3f9a5495eea9b97dbf885ef36944f2e420eff' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), h("span", { key: 'f889d29ed6c3d14bbc1d805888351d87f5122377', class: "action-sheet-button-inner" }, cancelButton.icon && (h("ion-icon", { key: '7c05cf424b38c37fd40aaeb42a494387291571fb', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && h("ion-ripple-effect", { key: 'bed927b477dc2708a5123ef560274fca9819b3d6' })))))), h("div", { key: 'c5df1b11dc15a93892d57065d3dd5fbe02e43b39', tabindex: "0", "aria-hidden": "true" })));
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" })));
218
396
  }
219
397
  static get is() { return "ion-action-sheet"; }
220
398
  static get encapsulation() { return "scoped"; }
@@ -568,6 +746,11 @@ export class ActionSheet {
568
746
  }
569
747
  };
570
748
  }
749
+ static get states() {
750
+ return {
751
+ "activeRadioId": {}
752
+ };
753
+ }
571
754
  static get events() {
572
755
  return [{
573
756
  "method": "didPresent",
@@ -822,6 +1005,9 @@ export class ActionSheet {
822
1005
  static get elementRef() { return "el"; }
823
1006
  static get watchers() {
824
1007
  return [{
1008
+ "propName": "buttons",
1009
+ "methodName": "buttonsChanged"
1010
+ }, {
825
1011
  "propName": "isOpen",
826
1012
  "methodName": "onIsOpenChange"
827
1013
  }, {
@@ -829,6 +1015,15 @@ export class ActionSheet {
829
1015
  "methodName": "triggerChanged"
830
1016
  }];
831
1017
  }
1018
+ static get listeners() {
1019
+ return [{
1020
+ "name": "keydown",
1021
+ "method": "onKeydown",
1022
+ "target": undefined,
1023
+ "capture": false,
1024
+ "passive": false
1025
+ }];
1026
+ }
832
1027
  }
833
1028
  const buttonClass = (button) => {
834
1029
  return Object.assign({ 'action-sheet-button': true, 'ion-activatable': !button.disabled, 'ion-focusable': !button.disabled, [`action-sheet-${button.role}`]: button.role !== undefined }, getClassMap(button.cssClass));
@@ -395,13 +395,18 @@ export class Select {
395
395
  .filter((cls) => cls !== 'hydrated')
396
396
  .join(' ');
397
397
  const optClass = `${OPTION_CLASS} ${copyClasses}`;
398
+ const isSelected = isOptionSelected(selectValue, value, this.compareWith);
398
399
  return {
399
- role: isOptionSelected(selectValue, value, this.compareWith) ? 'selected' : '',
400
+ role: isSelected ? 'selected' : '',
400
401
  text: option.textContent,
401
402
  cssClass: optClass,
402
403
  handler: () => {
403
404
  this.setValue(value);
404
405
  },
406
+ htmlAttributes: {
407
+ 'aria-checked': isSelected ? 'true' : 'false',
408
+ role: 'radio',
409
+ },
405
410
  };
406
411
  });
407
412
  // Add "cancel" button
@@ -828,7 +833,7 @@ export class Select {
828
833
  * TODO(FW-5592): Remove hasStartEndSlots condition
829
834
  */
830
835
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
831
- return (h(Host, { key: '35b5e18e6f79a802ff2d46d1242e80ff755cc0b9', onClick: this.onClick, class: createColorClasses(this.color, {
836
+ return (h(Host, { key: 'd8026835993d0e6dce747098f741a06ae4e4f54d', onClick: this.onClick, class: createColorClasses(this.color, {
832
837
  [mode]: true,
833
838
  'in-item': inItem,
834
839
  'in-item-color': hostContext('ion-item.ion-color', el),
@@ -846,7 +851,7 @@ export class Select {
846
851
  [`select-justify-${justify}`]: justifyEnabled,
847
852
  [`select-shape-${shape}`]: shape !== undefined,
848
853
  [`select-label-placement-${labelPlacement}`]: true,
849
- }) }, h("label", { key: '6005b34a0c50bc4d7653a4276bc232ecd02e083c', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'c7e07aa81ae856c057f16275dd058f37c5670a47', class: "select-wrapper-inner" }, h("slot", { key: '7fc2deefe0424404caacdbbd9e08ed43ba55d28a', name: "start" }), h("div", { key: '157d74ee717b1bc30b5f1c233a09b0c8456aa68e', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: 'ea66db304528b82bf9317730b6dce3db2612f235', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: '786eb1530b7476f0615d4e7c0bf4e7e4dc66509c', class: "select-highlight" })), this.renderBottomContent()));
854
+ }) }, h("label", { key: 'fcfb40209d6d07d49c7fdca4884b31abf6ac2567', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'f191664f2290c3890bde1156157c83a6ff17dbe2', class: "select-wrapper-inner" }, h("slot", { key: '317a28d1115b4214f291e228ce0fe6fc782e57d5', name: "start" }), h("div", { key: 'db68e18abd5ca3a1023d7c7b58bf89893ae18073', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: '4274e042267c2234a198b0f65c89477898d08130', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: '2e2eb1ee2b2791e0683d9afb186fde6e938ca59c', class: "select-highlight" })), this.renderBottomContent()));
850
855
  }
851
856
  static get is() { return "ion-select"; }
852
857
  static get encapsulation() { return "shadow"; }
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2025-12-08T06:09:31",
2
+ "timestamp": "2025-12-09T06:08:58",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.38.0",
@@ -1188,7 +1188,13 @@
1188
1188
  "docsTags": []
1189
1189
  }
1190
1190
  ],
1191
- "listeners": [],
1191
+ "listeners": [
1192
+ {
1193
+ "event": "keydown",
1194
+ "capture": false,
1195
+ "passive": false
1196
+ }
1197
+ ],
1192
1198
  "styles": [
1193
1199
  {
1194
1200
  "name": "--backdrop-opacity",
@@ -1485,15 +1491,15 @@
1485
1491
  "ion-select"
1486
1492
  ],
1487
1493
  "dependencies": [
1488
- "ion-backdrop",
1489
1494
  "ion-icon",
1490
- "ion-ripple-effect"
1495
+ "ion-ripple-effect",
1496
+ "ion-backdrop"
1491
1497
  ],
1492
1498
  "dependencyGraph": {
1493
1499
  "ion-action-sheet": [
1494
- "ion-backdrop",
1495
1500
  "ion-icon",
1496
- "ion-ripple-effect"
1501
+ "ion-ripple-effect",
1502
+ "ion-backdrop"
1497
1503
  ],
1498
1504
  "ion-select": [
1499
1505
  "ion-action-sheet"
@@ -31731,9 +31737,9 @@
31731
31737
  "ion-backdrop"
31732
31738
  ],
31733
31739
  "ion-action-sheet": [
31734
- "ion-backdrop",
31735
31740
  "ion-icon",
31736
- "ion-ripple-effect"
31741
+ "ion-ripple-effect",
31742
+ "ion-backdrop"
31737
31743
  ],
31738
31744
  "ion-alert": [
31739
31745
  "ion-ripple-effect",
@@ -119,6 +119,7 @@ const ActionSheet = class {
119
119
  this.delegateController = createDelegateController(this);
120
120
  this.lockController = createLockController();
121
121
  this.triggerController = createTriggerController();
122
+ this.hasRadioButtons = false;
122
123
  this.presented = false;
123
124
  /** @internal */
124
125
  this.hasController = false;
@@ -163,6 +164,19 @@ const ActionSheet = class {
163
164
  }
164
165
  };
165
166
  }
167
+ buttonsChanged() {
168
+ const radioButtons = this.getRadioButtons();
169
+ this.hasRadioButtons = radioButtons.length > 0;
170
+ // Initialize activeRadioId when buttons change
171
+ if (this.hasRadioButtons) {
172
+ const checkedButton = radioButtons.find((b) => { var _a; return ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a['aria-checked']) === 'true'; });
173
+ if (checkedButton) {
174
+ const allButtons = this.getButtons();
175
+ const checkedIndex = allButtons.indexOf(checkedButton);
176
+ this.activeRadioId = this.getButtonId(checkedButton, checkedIndex);
177
+ }
178
+ }
179
+ }
166
180
  onIsOpenChange(newValue, oldValue) {
167
181
  if (newValue === true && oldValue === false) {
168
182
  this.present();
@@ -243,11 +257,122 @@ const ActionSheet = class {
243
257
  }
244
258
  return true;
245
259
  }
260
+ /**
261
+ * Get all buttons regardless of role.
262
+ */
246
263
  getButtons() {
247
264
  return this.buttons.map((b) => {
248
265
  return typeof b === 'string' ? { text: b } : b;
249
266
  });
250
267
  }
268
+ /**
269
+ * Get all radio buttons (buttons with role="radio").
270
+ */
271
+ getRadioButtons() {
272
+ return this.getButtons().filter((b) => {
273
+ var _a;
274
+ const role = (_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role;
275
+ return role === 'radio' && !isCancel(role);
276
+ });
277
+ }
278
+ /**
279
+ * Handle radio button selection and update aria-checked state.
280
+ *
281
+ * @param button The radio button that was selected.
282
+ */
283
+ selectRadioButton(button) {
284
+ const buttonId = this.getButtonId(button);
285
+ // Set the active radio ID (this will trigger a re-render and update aria-checked)
286
+ this.activeRadioId = buttonId;
287
+ }
288
+ /**
289
+ * Get or generate an ID for a button.
290
+ *
291
+ * @param button The button for which to get the ID.
292
+ * @param index Optional index of the button in the buttons array.
293
+ * @returns The ID of the button.
294
+ */
295
+ getButtonId(button, index) {
296
+ if (button.id) {
297
+ return button.id;
298
+ }
299
+ const allButtons = this.getButtons();
300
+ const buttonIndex = index !== undefined ? index : allButtons.indexOf(button);
301
+ return `action-sheet-button-${this.overlayIndex}-${buttonIndex}`;
302
+ }
303
+ /**
304
+ * When the action sheet has radio buttons, we want to follow the
305
+ * keyboard navigation pattern for radio groups:
306
+ * - Arrow Down/Right: Move to the next radio button (wrap to first if at end)
307
+ * - Arrow Up/Left: Move to the previous radio button (wrap to last if at start)
308
+ * - Space/Enter: Select the focused radio button and trigger its handler
309
+ */
310
+ onKeydown(ev) {
311
+ // Only handle keyboard navigation if we have radio buttons
312
+ if (!this.hasRadioButtons || !this.presented) {
313
+ return;
314
+ }
315
+ const target = ev.target;
316
+ // Ignore if the target element is not within the action sheet or not a radio button
317
+ if (!this.el.contains(target) ||
318
+ !target.classList.contains('action-sheet-button') ||
319
+ target.getAttribute('role') !== 'radio') {
320
+ return;
321
+ }
322
+ // Get all radio button elements and filter out disabled ones
323
+ const radios = Array.from(this.el.querySelectorAll('.action-sheet-button[role="radio"]')).filter((el) => !el.disabled);
324
+ const currentIndex = radios.findIndex((radio) => radio.id === target.id);
325
+ if (currentIndex === -1) {
326
+ return;
327
+ }
328
+ const allButtons = this.getButtons();
329
+ const radioButtons = this.getRadioButtons();
330
+ /**
331
+ * Build a map of button element IDs to their ActionSheetButton
332
+ * config objects.
333
+ * This allows us to quickly look up which button config corresponds
334
+ * to a DOM element when handling keyboard navigation
335
+ * (e.g., whenuser presses Space/Enter or arrow keys).
336
+ * The key is the ID that was set on the DOM element during render,
337
+ * and the value is the ActionSheetButton config that contains the
338
+ * handler and other properties.
339
+ */
340
+ const buttonIdMap = new Map();
341
+ radioButtons.forEach((b) => {
342
+ const allIndex = allButtons.indexOf(b);
343
+ const buttonId = this.getButtonId(b, allIndex);
344
+ buttonIdMap.set(buttonId, b);
345
+ });
346
+ let nextEl;
347
+ if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
348
+ ev.preventDefault();
349
+ ev.stopPropagation();
350
+ nextEl = currentIndex === radios.length - 1 ? radios[0] : radios[currentIndex + 1];
351
+ }
352
+ else if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
353
+ ev.preventDefault();
354
+ ev.stopPropagation();
355
+ nextEl = currentIndex === 0 ? radios[radios.length - 1] : radios[currentIndex - 1];
356
+ }
357
+ else if (ev.key === ' ' || ev.key === 'Enter') {
358
+ ev.preventDefault();
359
+ ev.stopPropagation();
360
+ const button = buttonIdMap.get(target.id);
361
+ if (button) {
362
+ this.selectRadioButton(button);
363
+ this.buttonClick(button);
364
+ }
365
+ return;
366
+ }
367
+ // Focus the next radio button
368
+ if (nextEl) {
369
+ const button = buttonIdMap.get(nextEl.id);
370
+ if (button) {
371
+ this.selectRadioButton(button);
372
+ nextEl.focus();
373
+ }
374
+ }
375
+ }
251
376
  connectedCallback() {
252
377
  prepareOverlay(this.el);
253
378
  this.triggerChanged();
@@ -264,6 +389,8 @@ const ActionSheet = class {
264
389
  if (!((_a = this.htmlAttributes) === null || _a === void 0 ? void 0 : _a.id)) {
265
390
  setOverlayId(this.el);
266
391
  }
392
+ // Initialize activeRadioId for radio buttons
393
+ this.buttonsChanged();
267
394
  }
268
395
  componentDidLoad() {
269
396
  /**
@@ -301,22 +428,74 @@ const ActionSheet = class {
301
428
  */
302
429
  this.triggerChanged();
303
430
  }
431
+ renderActionSheetButtons(filteredButtons) {
432
+ const mode = getIonMode(this);
433
+ const { activeRadioId } = this;
434
+ return filteredButtons.map((b, index) => {
435
+ var _a;
436
+ const isRadio = ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role) === 'radio';
437
+ const buttonId = this.getButtonId(b, index);
438
+ const radioButtons = this.getRadioButtons();
439
+ const isActiveRadio = isRadio && buttonId === activeRadioId;
440
+ const isFirstRadio = isRadio && b === radioButtons[0];
441
+ // For radio buttons, set tabindex: 0 for the active one, -1 for others
442
+ // For non-radio buttons, use default tabindex (undefined, which means 0)
443
+ /**
444
+ * For radio buttons, set tabindex based on activeRadioId
445
+ * - If the button is the active radio, tabindex is 0
446
+ * - If no radio is active, the first radio button should have tabindex 0
447
+ * - All other radio buttons have tabindex -1
448
+ * For non-radio buttons, use default tabindex (undefined, which means 0)
449
+ */
450
+ let tabIndex;
451
+ if (isRadio) {
452
+ // Focus on the active radio button
453
+ if (isActiveRadio) {
454
+ tabIndex = 0;
455
+ }
456
+ else if (!activeRadioId && isFirstRadio) {
457
+ // No active radio, first radio gets focus
458
+ tabIndex = 0;
459
+ }
460
+ else {
461
+ // All other radios are not focusable
462
+ tabIndex = -1;
463
+ }
464
+ }
465
+ else {
466
+ tabIndex = undefined;
467
+ }
468
+ // For radio buttons, set aria-checked based on activeRadioId
469
+ // Otherwise, use the value from htmlAttributes if provided
470
+ const htmlAttrs = Object.assign({}, b.htmlAttributes);
471
+ if (isRadio) {
472
+ htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
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: () => {
475
+ if (isRadio) {
476
+ this.selectRadioButton(b);
477
+ }
478
+ this.buttonClick(b);
479
+ }, disabled: b.disabled, tabIndex: tabIndex }), h("span", { class: "action-sheet-button-inner" }, b.icon && h("ion-icon", { icon: b.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" }), b.text), mode === 'md' && h("ion-ripple-effect", null)));
480
+ });
481
+ }
304
482
  render() {
305
- const { header, htmlAttributes, overlayIndex } = this;
483
+ const { header, htmlAttributes, overlayIndex, hasRadioButtons } = this;
306
484
  const mode = getIonMode(this);
307
485
  const allButtons = this.getButtons();
308
486
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
309
487
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
310
488
  const headerID = `action-sheet-${overlayIndex}-header`;
311
- return (h(Host, Object.assign({ key: '9fef156b2a1f09ca4a6c1fe1f37c374139bde03c', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
489
+ return (h(Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
312
490
  zIndex: `${20000 + this.overlayIndex}`,
313
- }, 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: '81cf3f7d19864e041813987b46d2d115b8466819', tappable: this.backdropDismiss }), h("div", { key: '791c6a976683646fc306a42c15c5078b6f06a45f', tabindex: "0", "aria-hidden": "true" }), h("div", { key: 'a350b489ef7852eab9dc2227ce6d92da27dd9bf9', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, h("div", { key: '69ba51ee13510c1a411d87cb4845b11b7302a36f', class: "action-sheet-container" }, h("div", { key: 'bded15b8306c36591e526f0f99e1eeabcbab3915', class: "action-sheet-group", ref: (el) => (this.groupEl = el) }, header !== undefined && (h("div", { key: '06b5147c0f6d9180fe8f12e75c9b4a0310226adc', 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: '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: {
314
492
  'action-sheet-title': true,
315
493
  'action-sheet-has-sub-title': this.subHeader !== undefined,
316
- } }, header, this.subHeader && h("div", { key: '54874362a75c679aba803bf4f8768f5404d2dd28', class: "action-sheet-sub-title" }, this.subHeader))), buttons.map((b) => (h("button", Object.assign({}, b.htmlAttributes, { type: "button", id: b.id, class: buttonClass(b), onClick: () => this.buttonClick(b), disabled: b.disabled }), h("span", { class: "action-sheet-button-inner" }, b.icon && h("ion-icon", { icon: b.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" }), b.text), mode === 'md' && h("ion-ripple-effect", null))))), cancelButton && (h("div", { key: '67b0de298eb424f3dea846a841b7a06d70e3930d', class: "action-sheet-group action-sheet-group-cancel" }, h("button", Object.assign({ key: 'e7e3f9a5495eea9b97dbf885ef36944f2e420eff' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), h("span", { key: 'f889d29ed6c3d14bbc1d805888351d87f5122377', class: "action-sheet-button-inner" }, cancelButton.icon && (h("ion-icon", { key: '7c05cf424b38c37fd40aaeb42a494387291571fb', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && h("ion-ripple-effect", { key: 'bed927b477dc2708a5123ef560274fca9819b3d6' })))))), h("div", { key: 'c5df1b11dc15a93892d57065d3dd5fbe02e43b39', tabindex: "0", "aria-hidden": "true" })));
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" })));
317
495
  }
318
496
  get el() { return getElement(this); }
319
497
  static get watchers() { return {
498
+ "buttons": ["buttonsChanged"],
320
499
  "isOpen": ["onIsOpenChange"],
321
500
  "trigger": ["triggerChanged"]
322
501
  }; }
@@ -395,13 +395,18 @@ const Select = class {
395
395
  .filter((cls) => cls !== 'hydrated')
396
396
  .join(' ');
397
397
  const optClass = `${OPTION_CLASS} ${copyClasses}`;
398
+ const isSelected = isOptionSelected(selectValue, value, this.compareWith);
398
399
  return {
399
- role: isOptionSelected(selectValue, value, this.compareWith) ? 'selected' : '',
400
+ role: isSelected ? 'selected' : '',
400
401
  text: option.textContent,
401
402
  cssClass: optClass,
402
403
  handler: () => {
403
404
  this.setValue(value);
404
405
  },
406
+ htmlAttributes: {
407
+ 'aria-checked': isSelected ? 'true' : 'false',
408
+ role: 'radio',
409
+ },
405
410
  };
406
411
  });
407
412
  // Add "cancel" button
@@ -782,7 +787,7 @@ const Select = class {
782
787
  * TODO(FW-5592): Remove hasStartEndSlots condition
783
788
  */
784
789
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
785
- return (h(Host, { key: '35b5e18e6f79a802ff2d46d1242e80ff755cc0b9', onClick: this.onClick, class: createColorClasses(this.color, {
790
+ return (h(Host, { key: 'd8026835993d0e6dce747098f741a06ae4e4f54d', onClick: this.onClick, class: createColorClasses(this.color, {
786
791
  [mode]: true,
787
792
  'in-item': inItem,
788
793
  'in-item-color': hostContext('ion-item.ion-color', el),
@@ -800,7 +805,7 @@ const Select = class {
800
805
  [`select-justify-${justify}`]: justifyEnabled,
801
806
  [`select-shape-${shape}`]: shape !== undefined,
802
807
  [`select-label-placement-${labelPlacement}`]: true,
803
- }) }, h("label", { key: '6005b34a0c50bc4d7653a4276bc232ecd02e083c', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'c7e07aa81ae856c057f16275dd058f37c5670a47', class: "select-wrapper-inner" }, h("slot", { key: '7fc2deefe0424404caacdbbd9e08ed43ba55d28a', name: "start" }), h("div", { key: '157d74ee717b1bc30b5f1c233a09b0c8456aa68e', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: 'ea66db304528b82bf9317730b6dce3db2612f235', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: '786eb1530b7476f0615d4e7c0bf4e7e4dc66509c', class: "select-highlight" })), this.renderBottomContent()));
808
+ }) }, h("label", { key: 'fcfb40209d6d07d49c7fdca4884b31abf6ac2567', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), h("div", { key: 'f191664f2290c3890bde1156157c83a6ff17dbe2', class: "select-wrapper-inner" }, h("slot", { key: '317a28d1115b4214f291e228ce0fe6fc782e57d5', name: "start" }), h("div", { key: 'db68e18abd5ca3a1023d7c7b58bf89893ae18073', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), h("slot", { key: '4274e042267c2234a198b0f65c89477898d08130', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && h("div", { key: '2e2eb1ee2b2791e0683d9afb186fde6e938ca59c', class: "select-highlight" })), this.renderBottomContent()));
804
809
  }
805
810
  get el() { return getElement(this); }
806
811
  static get watchers() { return {