@ionic/core 8.7.12-nightly.20251205 → 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.
- package/components/action-sheet.js +186 -5
- package/components/ion-select.js +8 -3
- package/dist/cjs/ion-action-sheet.cjs.entry.js +183 -4
- package/dist/cjs/ion-select_3.cjs.entry.js +8 -3
- package/dist/cjs/ionic.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/action-sheet/action-sheet.js +199 -4
- package/dist/collection/components/select/select.js +8 -3
- package/dist/docs.json +14 -8
- package/dist/esm/ion-action-sheet.entry.js +183 -4
- package/dist/esm/ion-select_3.entry.js +8 -3
- package/dist/esm/ionic.js +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-5837f29f.entry.js +4 -0
- package/dist/ionic/p-8edc7565.entry.js +4 -0
- package/dist/types/components/action-sheet/action-sheet.d.ts +37 -0
- package/hydrate/index.js +193 -8
- package/hydrate/index.mjs +193 -8
- package/package.json +4 -1
- package/dist/ionic/p-510d86e1.entry.js +0 -4
- package/dist/ionic/p-7380261c.entry.js +0 -4
|
@@ -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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
},
|
|
527
|
+
}, [[0, "keydown", "onKeydown"]], {
|
|
528
|
+
"buttons": ["buttonsChanged"],
|
|
348
529
|
"isOpen": ["onIsOpenChange"],
|
|
349
530
|
"trigger": ["triggerChanged"]
|
|
350
531
|
}]);
|
package/components/ion-select.js
CHANGED
|
@@ -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:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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:
|
|
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: '
|
|
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: '
|
|
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 {
|