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

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.
@@ -118,6 +118,7 @@ const ActionSheet = /*@__PURE__*/ proxyCustomElement(class ActionSheet extends H
118
118
  this.delegateController = createDelegateController(this);
119
119
  this.lockController = createLockController();
120
120
  this.triggerController = createTriggerController();
121
+ this.hasRadioButtons = false;
121
122
  this.presented = false;
122
123
  /** @internal */
123
124
  this.hasController = false;
@@ -162,6 +163,19 @@ const ActionSheet = /*@__PURE__*/ proxyCustomElement(class ActionSheet extends H
162
163
  }
163
164
  };
164
165
  }
166
+ buttonsChanged() {
167
+ const radioButtons = this.getRadioButtons();
168
+ this.hasRadioButtons = radioButtons.length > 0;
169
+ // Initialize activeRadioId when buttons change
170
+ if (this.hasRadioButtons) {
171
+ const checkedButton = radioButtons.find((b) => { var _a; return ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a['aria-checked']) === 'true'; });
172
+ if (checkedButton) {
173
+ const allButtons = this.getButtons();
174
+ const checkedIndex = allButtons.indexOf(checkedButton);
175
+ this.activeRadioId = this.getButtonId(checkedButton, checkedIndex);
176
+ }
177
+ }
178
+ }
165
179
  onIsOpenChange(newValue, oldValue) {
166
180
  if (newValue === true && oldValue === false) {
167
181
  this.present();
@@ -242,11 +256,122 @@ const ActionSheet = /*@__PURE__*/ proxyCustomElement(class ActionSheet extends H
242
256
  }
243
257
  return true;
244
258
  }
259
+ /**
260
+ * Get all buttons regardless of role.
261
+ */
245
262
  getButtons() {
246
263
  return this.buttons.map((b) => {
247
264
  return typeof b === 'string' ? { text: b } : b;
248
265
  });
249
266
  }
267
+ /**
268
+ * Get all radio buttons (buttons with role="radio").
269
+ */
270
+ getRadioButtons() {
271
+ return this.getButtons().filter((b) => {
272
+ var _a;
273
+ const role = (_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role;
274
+ return role === 'radio' && !isCancel(role);
275
+ });
276
+ }
277
+ /**
278
+ * Handle radio button selection and update aria-checked state.
279
+ *
280
+ * @param button The radio button that was selected.
281
+ */
282
+ selectRadioButton(button) {
283
+ const buttonId = this.getButtonId(button);
284
+ // Set the active radio ID (this will trigger a re-render and update aria-checked)
285
+ this.activeRadioId = buttonId;
286
+ }
287
+ /**
288
+ * Get or generate an ID for a button.
289
+ *
290
+ * @param button The button for which to get the ID.
291
+ * @param index Optional index of the button in the buttons array.
292
+ * @returns The ID of the button.
293
+ */
294
+ getButtonId(button, index) {
295
+ if (button.id) {
296
+ return button.id;
297
+ }
298
+ const allButtons = this.getButtons();
299
+ const buttonIndex = index !== undefined ? index : allButtons.indexOf(button);
300
+ return `action-sheet-button-${this.overlayIndex}-${buttonIndex}`;
301
+ }
302
+ /**
303
+ * When the action sheet has radio buttons, we want to follow the
304
+ * keyboard navigation pattern for radio groups:
305
+ * - Arrow Down/Right: Move to the next radio button (wrap to first if at end)
306
+ * - Arrow Up/Left: Move to the previous radio button (wrap to last if at start)
307
+ * - Space/Enter: Select the focused radio button and trigger its handler
308
+ */
309
+ onKeydown(ev) {
310
+ // Only handle keyboard navigation if we have radio buttons
311
+ if (!this.hasRadioButtons || !this.presented) {
312
+ return;
313
+ }
314
+ const target = ev.target;
315
+ // Ignore if the target element is not within the action sheet or not a radio button
316
+ if (!this.el.contains(target) ||
317
+ !target.classList.contains('action-sheet-button') ||
318
+ target.getAttribute('role') !== 'radio') {
319
+ return;
320
+ }
321
+ // Get all radio button elements and filter out disabled ones
322
+ const radios = Array.from(this.el.querySelectorAll('.action-sheet-button[role="radio"]')).filter((el) => !el.disabled);
323
+ const currentIndex = radios.findIndex((radio) => radio.id === target.id);
324
+ if (currentIndex === -1) {
325
+ return;
326
+ }
327
+ const allButtons = this.getButtons();
328
+ const radioButtons = this.getRadioButtons();
329
+ /**
330
+ * Build a map of button element IDs to their ActionSheetButton
331
+ * config objects.
332
+ * This allows us to quickly look up which button config corresponds
333
+ * to a DOM element when handling keyboard navigation
334
+ * (e.g., whenuser presses Space/Enter or arrow keys).
335
+ * The key is the ID that was set on the DOM element during render,
336
+ * and the value is the ActionSheetButton config that contains the
337
+ * handler and other properties.
338
+ */
339
+ const buttonIdMap = new Map();
340
+ radioButtons.forEach((b) => {
341
+ const allIndex = allButtons.indexOf(b);
342
+ const buttonId = this.getButtonId(b, allIndex);
343
+ buttonIdMap.set(buttonId, b);
344
+ });
345
+ let nextEl;
346
+ if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
347
+ ev.preventDefault();
348
+ ev.stopPropagation();
349
+ nextEl = currentIndex === radios.length - 1 ? radios[0] : radios[currentIndex + 1];
350
+ }
351
+ else if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
352
+ ev.preventDefault();
353
+ ev.stopPropagation();
354
+ nextEl = currentIndex === 0 ? radios[radios.length - 1] : radios[currentIndex - 1];
355
+ }
356
+ else if (ev.key === ' ' || ev.key === 'Enter') {
357
+ ev.preventDefault();
358
+ ev.stopPropagation();
359
+ const button = buttonIdMap.get(target.id);
360
+ if (button) {
361
+ this.selectRadioButton(button);
362
+ this.buttonClick(button);
363
+ }
364
+ return;
365
+ }
366
+ // Focus the next radio button
367
+ if (nextEl) {
368
+ const button = buttonIdMap.get(nextEl.id);
369
+ if (button) {
370
+ this.selectRadioButton(button);
371
+ nextEl.focus();
372
+ }
373
+ }
374
+ }
250
375
  connectedCallback() {
251
376
  prepareOverlay(this.el);
252
377
  this.triggerChanged();
@@ -263,6 +388,8 @@ const ActionSheet = /*@__PURE__*/ proxyCustomElement(class ActionSheet extends H
263
388
  if (!((_a = this.htmlAttributes) === null || _a === void 0 ? void 0 : _a.id)) {
264
389
  setOverlayId(this.el);
265
390
  }
391
+ // Initialize activeRadioId for radio buttons
392
+ this.buttonsChanged();
266
393
  }
267
394
  componentDidLoad() {
268
395
  /**
@@ -300,22 +427,74 @@ const ActionSheet = /*@__PURE__*/ proxyCustomElement(class ActionSheet extends H
300
427
  */
301
428
  this.triggerChanged();
302
429
  }
430
+ renderActionSheetButtons(filteredButtons) {
431
+ const mode = getIonMode(this);
432
+ const { activeRadioId } = this;
433
+ return filteredButtons.map((b, index) => {
434
+ var _a;
435
+ const isRadio = ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role) === 'radio';
436
+ const buttonId = this.getButtonId(b, index);
437
+ const radioButtons = this.getRadioButtons();
438
+ const isActiveRadio = isRadio && buttonId === activeRadioId;
439
+ const isFirstRadio = isRadio && b === radioButtons[0];
440
+ // For radio buttons, set tabindex: 0 for the active one, -1 for others
441
+ // For non-radio buttons, use default tabindex (undefined, which means 0)
442
+ /**
443
+ * For radio buttons, set tabindex based on activeRadioId
444
+ * - If the button is the active radio, tabindex is 0
445
+ * - If no radio is active, the first radio button should have tabindex 0
446
+ * - All other radio buttons have tabindex -1
447
+ * For non-radio buttons, use default tabindex (undefined, which means 0)
448
+ */
449
+ let tabIndex;
450
+ if (isRadio) {
451
+ // Focus on the active radio button
452
+ if (isActiveRadio) {
453
+ tabIndex = 0;
454
+ }
455
+ else if (!activeRadioId && isFirstRadio) {
456
+ // No active radio, first radio gets focus
457
+ tabIndex = 0;
458
+ }
459
+ else {
460
+ // All other radios are not focusable
461
+ tabIndex = -1;
462
+ }
463
+ }
464
+ else {
465
+ tabIndex = undefined;
466
+ }
467
+ // For radio buttons, set aria-checked based on activeRadioId
468
+ // Otherwise, use the value from htmlAttributes if provided
469
+ const htmlAttrs = Object.assign({}, b.htmlAttributes);
470
+ if (isRadio) {
471
+ htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
472
+ }
473
+ 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
+ if (isRadio) {
475
+ this.selectRadioButton(b);
476
+ }
477
+ this.buttonClick(b);
478
+ }, 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)));
479
+ });
480
+ }
303
481
  render() {
304
- const { header, htmlAttributes, overlayIndex } = this;
482
+ const { header, htmlAttributes, overlayIndex, hasRadioButtons } = this;
305
483
  const mode = getIonMode(this);
306
484
  const allButtons = this.getButtons();
307
485
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
308
486
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
309
487
  const headerID = `action-sheet-${overlayIndex}-header`;
310
- return (h(Host, Object.assign({ key: '9fef156b2a1f09ca4a6c1fe1f37c374139bde03c', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
488
+ return (h(Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
311
489
  zIndex: `${20000 + this.overlayIndex}`,
312
- }, 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: {
490
+ }, 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: {
313
491
  'action-sheet-title': true,
314
492
  'action-sheet-has-sub-title': this.subHeader !== undefined,
315
- } }, 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" })));
493
+ } }, 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" })));
316
494
  }
317
495
  get el() { return this; }
318
496
  static get watchers() { return {
497
+ "buttons": ["buttonsChanged"],
319
498
  "isOpen": ["onIsOpenChange"],
320
499
  "trigger": ["triggerChanged"]
321
500
  }; }
@@ -340,11 +519,13 @@ const ActionSheet = /*@__PURE__*/ proxyCustomElement(class ActionSheet extends H
340
519
  "htmlAttributes": [16],
341
520
  "isOpen": [4, "is-open"],
342
521
  "trigger": [1],
522
+ "activeRadioId": [32],
343
523
  "present": [64],
344
524
  "dismiss": [64],
345
525
  "onDidDismiss": [64],
346
526
  "onWillDismiss": [64]
347
- }, undefined, {
527
+ }, [[0, "keydown", "onKeydown"]], {
528
+ "buttons": ["buttonsChanged"],
348
529
  "isOpen": ["onIsOpenChange"],
349
530
  "trigger": ["triggerChanged"]
350
531
  }]);
@@ -417,13 +417,18 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
417
417
  .filter((cls) => cls !== 'hydrated')
418
418
  .join(' ');
419
419
  const optClass = `${OPTION_CLASS} ${copyClasses}`;
420
+ const isSelected = isOptionSelected(selectValue, value, this.compareWith);
420
421
  return {
421
- role: isOptionSelected(selectValue, value, this.compareWith) ? 'selected' : '',
422
+ role: isSelected ? 'selected' : '',
422
423
  text: option.textContent,
423
424
  cssClass: optClass,
424
425
  handler: () => {
425
426
  this.setValue(value);
426
427
  },
428
+ htmlAttributes: {
429
+ 'aria-checked': isSelected ? 'true' : 'false',
430
+ role: 'radio',
431
+ },
427
432
  };
428
433
  });
429
434
  // Add "cancel" button
@@ -804,7 +809,7 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
804
809
  * TODO(FW-5592): Remove hasStartEndSlots condition
805
810
  */
806
811
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
807
- return (h(Host, { key: '35b5e18e6f79a802ff2d46d1242e80ff755cc0b9', onClick: this.onClick, class: createColorClasses(this.color, {
812
+ return (h(Host, { key: 'd8026835993d0e6dce747098f741a06ae4e4f54d', onClick: this.onClick, class: createColorClasses(this.color, {
808
813
  [mode]: true,
809
814
  'in-item': inItem,
810
815
  'in-item-color': hostContext('ion-item.ion-color', el),
@@ -822,7 +827,7 @@ const Select = /*@__PURE__*/ proxyCustomElement(class Select extends HTMLElement
822
827
  [`select-justify-${justify}`]: justifyEnabled,
823
828
  [`select-shape-${shape}`]: shape !== undefined,
824
829
  [`select-label-placement-${labelPlacement}`]: true,
825
- }) }, 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()));
830
+ }) }, 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()));
826
831
  }
827
832
  get el() { return this; }
828
833
  static get watchers() { return {
@@ -121,6 +121,7 @@ const ActionSheet = class {
121
121
  this.delegateController = overlays.createDelegateController(this);
122
122
  this.lockController = lockController.createLockController();
123
123
  this.triggerController = overlays.createTriggerController();
124
+ this.hasRadioButtons = false;
124
125
  this.presented = false;
125
126
  /** @internal */
126
127
  this.hasController = false;
@@ -165,6 +166,19 @@ const ActionSheet = class {
165
166
  }
166
167
  };
167
168
  }
169
+ buttonsChanged() {
170
+ const radioButtons = this.getRadioButtons();
171
+ this.hasRadioButtons = radioButtons.length > 0;
172
+ // Initialize activeRadioId when buttons change
173
+ if (this.hasRadioButtons) {
174
+ const checkedButton = radioButtons.find((b) => { var _a; return ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a['aria-checked']) === 'true'; });
175
+ if (checkedButton) {
176
+ const allButtons = this.getButtons();
177
+ const checkedIndex = allButtons.indexOf(checkedButton);
178
+ this.activeRadioId = this.getButtonId(checkedButton, checkedIndex);
179
+ }
180
+ }
181
+ }
168
182
  onIsOpenChange(newValue, oldValue) {
169
183
  if (newValue === true && oldValue === false) {
170
184
  this.present();
@@ -245,11 +259,122 @@ const ActionSheet = class {
245
259
  }
246
260
  return true;
247
261
  }
262
+ /**
263
+ * Get all buttons regardless of role.
264
+ */
248
265
  getButtons() {
249
266
  return this.buttons.map((b) => {
250
267
  return typeof b === 'string' ? { text: b } : b;
251
268
  });
252
269
  }
270
+ /**
271
+ * Get all radio buttons (buttons with role="radio").
272
+ */
273
+ getRadioButtons() {
274
+ return this.getButtons().filter((b) => {
275
+ var _a;
276
+ const role = (_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role;
277
+ return role === 'radio' && !overlays.isCancel(role);
278
+ });
279
+ }
280
+ /**
281
+ * Handle radio button selection and update aria-checked state.
282
+ *
283
+ * @param button The radio button that was selected.
284
+ */
285
+ selectRadioButton(button) {
286
+ const buttonId = this.getButtonId(button);
287
+ // Set the active radio ID (this will trigger a re-render and update aria-checked)
288
+ this.activeRadioId = buttonId;
289
+ }
290
+ /**
291
+ * Get or generate an ID for a button.
292
+ *
293
+ * @param button The button for which to get the ID.
294
+ * @param index Optional index of the button in the buttons array.
295
+ * @returns The ID of the button.
296
+ */
297
+ getButtonId(button, index) {
298
+ if (button.id) {
299
+ return button.id;
300
+ }
301
+ const allButtons = this.getButtons();
302
+ const buttonIndex = index !== undefined ? index : allButtons.indexOf(button);
303
+ return `action-sheet-button-${this.overlayIndex}-${buttonIndex}`;
304
+ }
305
+ /**
306
+ * When the action sheet has radio buttons, we want to follow the
307
+ * keyboard navigation pattern for radio groups:
308
+ * - Arrow Down/Right: Move to the next radio button (wrap to first if at end)
309
+ * - Arrow Up/Left: Move to the previous radio button (wrap to last if at start)
310
+ * - Space/Enter: Select the focused radio button and trigger its handler
311
+ */
312
+ onKeydown(ev) {
313
+ // Only handle keyboard navigation if we have radio buttons
314
+ if (!this.hasRadioButtons || !this.presented) {
315
+ return;
316
+ }
317
+ const target = ev.target;
318
+ // Ignore if the target element is not within the action sheet or not a radio button
319
+ if (!this.el.contains(target) ||
320
+ !target.classList.contains('action-sheet-button') ||
321
+ target.getAttribute('role') !== 'radio') {
322
+ return;
323
+ }
324
+ // Get all radio button elements and filter out disabled ones
325
+ const radios = Array.from(this.el.querySelectorAll('.action-sheet-button[role="radio"]')).filter((el) => !el.disabled);
326
+ const currentIndex = radios.findIndex((radio) => radio.id === target.id);
327
+ if (currentIndex === -1) {
328
+ return;
329
+ }
330
+ const allButtons = this.getButtons();
331
+ const radioButtons = this.getRadioButtons();
332
+ /**
333
+ * Build a map of button element IDs to their ActionSheetButton
334
+ * config objects.
335
+ * This allows us to quickly look up which button config corresponds
336
+ * to a DOM element when handling keyboard navigation
337
+ * (e.g., whenuser presses Space/Enter or arrow keys).
338
+ * The key is the ID that was set on the DOM element during render,
339
+ * and the value is the ActionSheetButton config that contains the
340
+ * handler and other properties.
341
+ */
342
+ const buttonIdMap = new Map();
343
+ radioButtons.forEach((b) => {
344
+ const allIndex = allButtons.indexOf(b);
345
+ const buttonId = this.getButtonId(b, allIndex);
346
+ buttonIdMap.set(buttonId, b);
347
+ });
348
+ let nextEl;
349
+ if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
350
+ ev.preventDefault();
351
+ ev.stopPropagation();
352
+ nextEl = currentIndex === radios.length - 1 ? radios[0] : radios[currentIndex + 1];
353
+ }
354
+ else if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
355
+ ev.preventDefault();
356
+ ev.stopPropagation();
357
+ nextEl = currentIndex === 0 ? radios[radios.length - 1] : radios[currentIndex - 1];
358
+ }
359
+ else if (ev.key === ' ' || ev.key === 'Enter') {
360
+ ev.preventDefault();
361
+ ev.stopPropagation();
362
+ const button = buttonIdMap.get(target.id);
363
+ if (button) {
364
+ this.selectRadioButton(button);
365
+ this.buttonClick(button);
366
+ }
367
+ return;
368
+ }
369
+ // Focus the next radio button
370
+ if (nextEl) {
371
+ const button = buttonIdMap.get(nextEl.id);
372
+ if (button) {
373
+ this.selectRadioButton(button);
374
+ nextEl.focus();
375
+ }
376
+ }
377
+ }
253
378
  connectedCallback() {
254
379
  overlays.prepareOverlay(this.el);
255
380
  this.triggerChanged();
@@ -266,6 +391,8 @@ const ActionSheet = class {
266
391
  if (!((_a = this.htmlAttributes) === null || _a === void 0 ? void 0 : _a.id)) {
267
392
  overlays.setOverlayId(this.el);
268
393
  }
394
+ // Initialize activeRadioId for radio buttons
395
+ this.buttonsChanged();
269
396
  }
270
397
  componentDidLoad() {
271
398
  /**
@@ -303,22 +430,74 @@ const ActionSheet = class {
303
430
  */
304
431
  this.triggerChanged();
305
432
  }
433
+ renderActionSheetButtons(filteredButtons) {
434
+ const mode = ionicGlobal.getIonMode(this);
435
+ const { activeRadioId } = this;
436
+ return filteredButtons.map((b, index$1) => {
437
+ var _a;
438
+ const isRadio = ((_a = b.htmlAttributes) === null || _a === void 0 ? void 0 : _a.role) === 'radio';
439
+ const buttonId = this.getButtonId(b, index$1);
440
+ const radioButtons = this.getRadioButtons();
441
+ const isActiveRadio = isRadio && buttonId === activeRadioId;
442
+ const isFirstRadio = isRadio && b === radioButtons[0];
443
+ // For radio buttons, set tabindex: 0 for the active one, -1 for others
444
+ // For non-radio buttons, use default tabindex (undefined, which means 0)
445
+ /**
446
+ * For radio buttons, set tabindex based on activeRadioId
447
+ * - If the button is the active radio, tabindex is 0
448
+ * - If no radio is active, the first radio button should have tabindex 0
449
+ * - All other radio buttons have tabindex -1
450
+ * For non-radio buttons, use default tabindex (undefined, which means 0)
451
+ */
452
+ let tabIndex;
453
+ if (isRadio) {
454
+ // Focus on the active radio button
455
+ if (isActiveRadio) {
456
+ tabIndex = 0;
457
+ }
458
+ else if (!activeRadioId && isFirstRadio) {
459
+ // No active radio, first radio gets focus
460
+ tabIndex = 0;
461
+ }
462
+ else {
463
+ // All other radios are not focusable
464
+ tabIndex = -1;
465
+ }
466
+ }
467
+ else {
468
+ tabIndex = undefined;
469
+ }
470
+ // For radio buttons, set aria-checked based on activeRadioId
471
+ // Otherwise, use the value from htmlAttributes if provided
472
+ const htmlAttrs = Object.assign({}, b.htmlAttributes);
473
+ if (isRadio) {
474
+ htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false';
475
+ }
476
+ return (index.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: () => {
477
+ if (isRadio) {
478
+ this.selectRadioButton(b);
479
+ }
480
+ this.buttonClick(b);
481
+ }, disabled: b.disabled, tabIndex: tabIndex }), index.h("span", { class: "action-sheet-button-inner" }, b.icon && index.h("ion-icon", { icon: b.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" }), b.text), mode === 'md' && index.h("ion-ripple-effect", null)));
482
+ });
483
+ }
306
484
  render() {
307
- const { header, htmlAttributes, overlayIndex } = this;
485
+ const { header, htmlAttributes, overlayIndex, hasRadioButtons } = this;
308
486
  const mode = ionicGlobal.getIonMode(this);
309
487
  const allButtons = this.getButtons();
310
488
  const cancelButton = allButtons.find((b) => b.role === 'cancel');
311
489
  const buttons = allButtons.filter((b) => b.role !== 'cancel');
312
490
  const headerID = `action-sheet-${overlayIndex}-header`;
313
- return (index.h(index.Host, Object.assign({ key: '9fef156b2a1f09ca4a6c1fe1f37c374139bde03c', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
491
+ return (index.h(index.Host, Object.assign({ key: '173fcff5b1da7c33c267de4667591c946b8c8d03', role: "dialog", "aria-modal": "true", "aria-labelledby": header !== undefined ? headerID : null, tabindex: "-1" }, htmlAttributes, { style: {
314
492
  zIndex: `${20000 + this.overlayIndex}`,
315
- }, class: Object.assign(Object.assign({ [mode]: true }, theme.getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), index.h("ion-backdrop", { key: '81cf3f7d19864e041813987b46d2d115b8466819', tappable: this.backdropDismiss }), index.h("div", { key: '791c6a976683646fc306a42c15c5078b6f06a45f', tabindex: "0", "aria-hidden": "true" }), index.h("div", { key: 'a350b489ef7852eab9dc2227ce6d92da27dd9bf9', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, index.h("div", { key: '69ba51ee13510c1a411d87cb4845b11b7302a36f', class: "action-sheet-container" }, index.h("div", { key: 'bded15b8306c36591e526f0f99e1eeabcbab3915', class: "action-sheet-group", ref: (el) => (this.groupEl = el) }, header !== undefined && (index.h("div", { key: '06b5147c0f6d9180fe8f12e75c9b4a0310226adc', id: headerID, class: {
493
+ }, class: Object.assign(Object.assign({ [mode]: true }, theme.getClassMap(this.cssClass)), { 'overlay-hidden': true, 'action-sheet-translucent': this.translucent }), onIonActionSheetWillDismiss: this.dispatchCancelHandler, onIonBackdropTap: this.onBackdropTap }), index.h("ion-backdrop", { key: '521ede659f747864f6c974e09016436eceb7158c', tappable: this.backdropDismiss }), index.h("div", { key: '7a7946fc434bc444f16a70638f5e948c69d33fcd', tabindex: "0", "aria-hidden": "true" }), index.h("div", { key: 'bcff39a580489dbafa255842e57aa8602c6d0f18', class: "action-sheet-wrapper ion-overlay-wrapper", ref: (el) => (this.wrapperEl = el) }, index.h("div", { key: '84bba13ce14261f0f0daa3f9c77648c9e7f36e0e', class: "action-sheet-container" }, index.h("div", { key: 'd9c8ac404fd6719a7adf8cb36549f67616f9a0c4', class: "action-sheet-group", ref: (el) => (this.groupEl = el), role: hasRadioButtons ? 'radiogroup' : undefined }, header !== undefined && (index.h("div", { key: '180433a8ad03ef5c54728a1a8f34715b6921d658', id: headerID, class: {
316
494
  'action-sheet-title': true,
317
495
  'action-sheet-has-sub-title': this.subHeader !== undefined,
318
- } }, header, this.subHeader && index.h("div", { key: '54874362a75c679aba803bf4f8768f5404d2dd28', class: "action-sheet-sub-title" }, this.subHeader))), buttons.map((b) => (index.h("button", Object.assign({}, b.htmlAttributes, { type: "button", id: b.id, class: buttonClass(b), onClick: () => this.buttonClick(b), disabled: b.disabled }), index.h("span", { class: "action-sheet-button-inner" }, b.icon && index.h("ion-icon", { icon: b.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" }), b.text), mode === 'md' && index.h("ion-ripple-effect", null))))), cancelButton && (index.h("div", { key: '67b0de298eb424f3dea846a841b7a06d70e3930d', class: "action-sheet-group action-sheet-group-cancel" }, index.h("button", Object.assign({ key: 'e7e3f9a5495eea9b97dbf885ef36944f2e420eff' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), index.h("span", { key: 'f889d29ed6c3d14bbc1d805888351d87f5122377', class: "action-sheet-button-inner" }, cancelButton.icon && (index.h("ion-icon", { key: '7c05cf424b38c37fd40aaeb42a494387291571fb', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && index.h("ion-ripple-effect", { key: 'bed927b477dc2708a5123ef560274fca9819b3d6' })))))), index.h("div", { key: 'c5df1b11dc15a93892d57065d3dd5fbe02e43b39', tabindex: "0", "aria-hidden": "true" })));
496
+ } }, header, this.subHeader && index.h("div", { key: '7138e79e61b1a8f42bc5a9175c57fa2f15d7ec5a', class: "action-sheet-sub-title" }, this.subHeader))), this.renderActionSheetButtons(buttons)), cancelButton && (index.h("div", { key: 'b617c722f5b8028d73ed34b69310f312c65f34a7', class: "action-sheet-group action-sheet-group-cancel" }, index.h("button", Object.assign({ key: 'd0dd876fc48815df3710413c201c0b445a8e16c0' }, cancelButton.htmlAttributes, { type: "button", class: buttonClass(cancelButton), onClick: () => this.buttonClick(cancelButton) }), index.h("span", { key: 'e7b960157cc6fc5fe92a12090b2be55e8ae072e4', class: "action-sheet-button-inner" }, cancelButton.icon && (index.h("ion-icon", { key: '05498ffc60cab911dbff0ecbc6168dea59ada9a5', icon: cancelButton.icon, "aria-hidden": "true", lazy: false, class: "action-sheet-icon" })), cancelButton.text), mode === 'md' && index.h("ion-ripple-effect", { key: '3d401346cea301be4ca03671f7370f6f4b0b6bde' })))))), index.h("div", { key: '971f3c5fcc07f36c28eb469a47ec0290c692e139', tabindex: "0", "aria-hidden": "true" })));
319
497
  }
320
498
  get el() { return index.getElement(this); }
321
499
  static get watchers() { return {
500
+ "buttons": ["buttonsChanged"],
322
501
  "isOpen": ["onIsOpenChange"],
323
502
  "trigger": ["triggerChanged"]
324
503
  }; }
@@ -397,13 +397,18 @@ const Select = class {
397
397
  .filter((cls) => cls !== 'hydrated')
398
398
  .join(' ');
399
399
  const optClass = `${OPTION_CLASS} ${copyClasses}`;
400
+ const isSelected = compareWithUtils.isOptionSelected(selectValue, value, this.compareWith);
400
401
  return {
401
- role: compareWithUtils.isOptionSelected(selectValue, value, this.compareWith) ? 'selected' : '',
402
+ role: isSelected ? 'selected' : '',
402
403
  text: option.textContent,
403
404
  cssClass: optClass,
404
405
  handler: () => {
405
406
  this.setValue(value);
406
407
  },
408
+ htmlAttributes: {
409
+ 'aria-checked': isSelected ? 'true' : 'false',
410
+ role: 'radio',
411
+ },
407
412
  };
408
413
  });
409
414
  // Add "cancel" button
@@ -784,7 +789,7 @@ const Select = class {
784
789
  * TODO(FW-5592): Remove hasStartEndSlots condition
785
790
  */
786
791
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots));
787
- return (index.h(index.Host, { key: '35b5e18e6f79a802ff2d46d1242e80ff755cc0b9', onClick: this.onClick, class: theme.createColorClasses(this.color, {
792
+ return (index.h(index.Host, { key: 'd8026835993d0e6dce747098f741a06ae4e4f54d', onClick: this.onClick, class: theme.createColorClasses(this.color, {
788
793
  [mode]: true,
789
794
  'in-item': inItem,
790
795
  'in-item-color': theme.hostContext('ion-item.ion-color', el),
@@ -802,7 +807,7 @@ const Select = class {
802
807
  [`select-justify-${justify}`]: justifyEnabled,
803
808
  [`select-shape-${shape}`]: shape !== undefined,
804
809
  [`select-label-placement-${labelPlacement}`]: true,
805
- }) }, index.h("label", { key: '6005b34a0c50bc4d7653a4276bc232ecd02e083c', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: 'c7e07aa81ae856c057f16275dd058f37c5670a47', class: "select-wrapper-inner" }, index.h("slot", { key: '7fc2deefe0424404caacdbbd9e08ed43ba55d28a', name: "start" }), index.h("div", { key: '157d74ee717b1bc30b5f1c233a09b0c8456aa68e', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), index.h("slot", { key: 'ea66db304528b82bf9317730b6dce3db2612f235', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && index.h("div", { key: '786eb1530b7476f0615d4e7c0bf4e7e4dc66509c', class: "select-highlight" })), this.renderBottomContent()));
810
+ }) }, index.h("label", { key: 'fcfb40209d6d07d49c7fdca4884b31abf6ac2567', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick }, this.renderLabelContainer(), index.h("div", { key: 'f191664f2290c3890bde1156157c83a6ff17dbe2', class: "select-wrapper-inner" }, index.h("slot", { key: '317a28d1115b4214f291e228ce0fe6fc782e57d5', name: "start" }), index.h("div", { key: 'db68e18abd5ca3a1023d7c7b58bf89893ae18073', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox()), index.h("slot", { key: '4274e042267c2234a198b0f65c89477898d08130', name: "end" }), !hasFloatingOrStackedLabel && this.renderSelectIcon()), hasFloatingOrStackedLabel && this.renderSelectIcon(), shouldRenderHighlight && index.h("div", { key: '2e2eb1ee2b2791e0683d9afb186fde6e938ca59c', class: "select-highlight" })), this.renderBottomContent()));
806
811
  }
807
812
  get el() { return index.getElement(this); }
808
813
  static get watchers() { return {