From d5197be9fad56cd22572b52e65cd824c06e367fa Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 4 Nov 2025 12:07:36 -0800 Subject: [PATCH 1/8] fix(select): add aria-selected to button option --- core/src/components/select/select.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index ac2fbb6d89f..10d6836c262 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -548,6 +548,7 @@ export class Select implements ComponentInterface { } private createActionSheetButtons(data: HTMLIonSelectOptionElement[], selectValue: any): ActionSheetButton[] { + console.log('createActionSheetButtons', data, selectValue); const actionSheetButtons = data.map((option) => { const value = getOptionValue(option); @@ -556,14 +557,18 @@ export class Select implements ComponentInterface { .filter((cls) => cls !== 'hydrated') .join(' '); const optClass = `${OPTION_CLASS} ${copyClasses}`; + const isSelected = isOptionSelected(selectValue, value, this.compareWith); return { - role: isOptionSelected(selectValue, value, this.compareWith) ? 'selected' : '', + role: isSelected ? 'selected' : '', text: option.textContent, cssClass: optClass, handler: () => { this.setValue(value); }, + htmlAttributes: { + 'aria-selected': isSelected ? 'true' : undefined, + }, } as ActionSheetButton; }); From 8df50f4a9fd66e2fff6a7a79baba6e30a2fe5c32 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 4 Nov 2025 15:58:28 -0800 Subject: [PATCH 2/8] fix(select): use aria description for selected option --- core/src/components/select/select.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 10d6836c262..ddc95af0293 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -566,9 +566,7 @@ export class Select implements ComponentInterface { handler: () => { this.setValue(value); }, - htmlAttributes: { - 'aria-selected': isSelected ? 'true' : undefined, - }, + ...(isSelected ? { htmlAttributes: { 'aria-description': 'selected' } } : {}), } as ActionSheetButton; }); From 398ae6d8cdd2016cbb4cda831ccc604269c41dd4 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 5 Nov 2025 10:17:44 -0800 Subject: [PATCH 3/8] docs(select): remove comment --- core/src/components/select/select.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index ddc95af0293..2b12082c505 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -548,7 +548,6 @@ export class Select implements ComponentInterface { } private createActionSheetButtons(data: HTMLIonSelectOptionElement[], selectValue: any): ActionSheetButton[] { - console.log('createActionSheetButtons', data, selectValue); const actionSheetButtons = data.map((option) => { const value = getOptionValue(option); From 3f3ffaec1d9dfa7ac3ff08d455dd527d8ab5e8d3 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 5 Nov 2025 12:41:32 -0800 Subject: [PATCH 4/8] test(select): check for aria description --- .../components/select/test/a11y/select.e2e.ts | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/core/src/components/select/test/a11y/select.e2e.ts b/core/src/components/select/test/a11y/select.e2e.ts index 063add891dc..55416a982c4 100644 --- a/core/src/components/select/test/a11y/select.e2e.ts +++ b/core/src/components/select/test/a11y/select.e2e.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test'; import { configs, test } from '@utils/test/playwright'; configs({ directions: ['ltr'], palettes: ['light', 'dark'] }).forEach(({ title, config }) => { - test.describe(title('textarea: a11y'), () => { + test.describe(title('select: a11y'), () => { test('default layout should not have accessibility violations', async ({ page }) => { await page.setContent( ` @@ -111,3 +111,39 @@ configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => { }); }); }); + +/** + * This behavior does not vary across modes/directions. + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('select: aria attributes'), () => { + test('should have a aria-description on the selected option when action sheet interface is open', async ({ + page, + }) => { + await page.setContent( + ` + + Apple + Banana + Orange + + `, + config + ); + + const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent'); + + const select = page.locator('ion-select'); + + await select.click(); + await ionActionSheetDidPresent.next(); + + const selectedOption = page.locator('.action-sheet-selected'); + await expect(selectedOption).toHaveAttribute('aria-description', 'selected'); + + // Check that the attribut is not added to non-selected option + const nonSelectedOption = page.locator('.select-interface-option:not(.action-sheet-selected)').first(); + await expect(nonSelectedOption).not.toHaveAttribute('aria-description'); + }); + }); +}); From e1f83b06714c2971d80af70278e893e9bb70633f Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 12 Nov 2025 14:41:53 -0800 Subject: [PATCH 5/8] refactor(action-sheet): accept radio role buttons Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> --- .../action-sheet/action-sheet-interface.ts | 2 +- .../components/action-sheet/action-sheet.tsx | 247 ++++++++++++++++-- .../test/a11y/action-sheet.e2e.ts | 55 ++++ .../action-sheet/test/a11y/index.html | 27 ++ core/src/components/select/select.tsx | 6 +- .../components/select/test/a11y/select.e2e.ts | 36 --- 6 files changed, 315 insertions(+), 58 deletions(-) diff --git a/core/src/components/action-sheet/action-sheet-interface.ts b/core/src/components/action-sheet/action-sheet-interface.ts index f9cc06cd32f..e8d199796fd 100644 --- a/core/src/components/action-sheet/action-sheet-interface.ts +++ b/core/src/components/action-sheet/action-sheet-interface.ts @@ -19,7 +19,7 @@ export interface ActionSheetOptions { export interface ActionSheetButton { text?: string; - role?: LiteralUnion<'cancel' | 'destructive' | 'selected', string>; + role?: LiteralUnion<'cancel' | 'destructive' | 'selected' | 'radio', string>; icon?: string; cssClass?: string | string[]; id?: string; diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index dcd7c03847b..78a6637723c 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -1,5 +1,5 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Watch, Component, Element, Event, Host, Method, Prop, h, readTask } from '@stencil/core'; +import { Watch, Component, Element, Event, Host, Listen, Method, Prop, State, h, readTask } from '@stencil/core'; import type { Gesture } from '@utils/gesture'; import { createButtonActiveGesture } from '@utils/gesture/button-active'; import { raf } from '@utils/helpers'; @@ -46,11 +46,18 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { private wrapperEl?: HTMLElement; private groupEl?: HTMLElement; private gesture?: Gesture; + private hasRadioButtons = false; presented = false; lastFocus?: HTMLElement; animation?: any; + /** + * The ID of the currently active/selected radio button. + * Used for keyboard navigation and ARIA attributes. + */ + @State() activeRadioId?: string; + @Element() el!: HTMLIonActionSheetElement; /** @internal */ @@ -81,6 +88,19 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { * An array of buttons for the action sheet. */ @Prop() buttons: (ActionSheetButton | string)[] = []; + @Watch('buttons') + buttonsChanged() { + // Initialize activeRadioId when buttons change + if (this.hasRadioButtons) { + const allButtons = this.getButtons(); + const radioButtons = this.getRadioButtons(); + const checkedButton = radioButtons.find((b) => b.htmlAttributes?.['aria-checked'] === 'true'); + if (checkedButton) { + const checkedIndex = allButtons.indexOf(checkedButton); + this.activeRadioId = this.getButtonId(checkedButton, checkedIndex); + } + } + } /** * Additional classes to apply for custom CSS. If multiple classes are @@ -277,12 +297,50 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { return true; } + /** + * Get all buttons regardless of role. + */ private getButtons(): ActionSheetButton[] { return this.buttons.map((b) => { return typeof b === 'string' ? { text: b } : b; }); } + /** + * Get all radio buttons (buttons with role="radio"). + */ + private getRadioButtons(): ActionSheetButton[] { + return this.getButtons().filter((b) => b.role === 'radio' && !isCancel(b.role)); + } + + /** + * Handle radio button selection and update aria-checked state. + * + * @param button The radio button that was selected. + */ + private async selectRadioButton(button: ActionSheetButton) { + const buttonId = this.getButtonId(button); + + // Set the active radio ID (this will trigger a re-render and update aria-checked) + this.activeRadioId = buttonId; + } + + /** + * Get or generate an ID for a button. + * + * @param button The button for which to get the ID. + * @param index Optional index of the button in the buttons array. + * @returns The ID of the button. + */ + private getButtonId(button: ActionSheetButton, index?: number): string { + if (button.id) { + return button.id; + } + const allButtons = this.getButtons(); + const buttonIndex = index !== undefined ? index : allButtons.indexOf(button); + return `action-sheet-button-${this.overlayIndex}-${buttonIndex}`; + } + private onBackdropTap = () => { this.dismiss(undefined, BACKDROP); }; @@ -295,9 +353,94 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { } }; + /** + * When the action sheet has radio buttons, we want to follow the + * keyboard navigation pattern for radio groups: + * - Arrow Down/Right: Move to the next radio button (wrap to first if at end) + * - Arrow Up/Left: Move to the previous radio button (wrap to last if at start) + * - Space/Enter: Select the focused radio button and trigger its handler + */ + @Listen('keydown') + onKeydown(ev: KeyboardEvent) { + // Only handle keyboard navigation if we have radio buttons + if (!this.hasRadioButtons || !this.presented) { + return; + } + + const target = ev.target as HTMLElement; + + // Ignore if the target element is not within the action sheet or not a radio button + if ( + !this.el.contains(target) || + !target.classList.contains('action-sheet-button') || + target.getAttribute('role') !== 'radio' + ) { + return; + } + + // Get all radio button elements and filter out disabled ones + const radios = Array.from(this.el.querySelectorAll('.action-sheet-button[role="radio"]')).filter( + (el) => !(el as HTMLButtonElement).disabled + ) as HTMLButtonElement[]; + + const currentIndex = radios.findIndex((radio) => radio.id === target.id); + if (currentIndex === -1) { + return; + } + + let nextEl: HTMLButtonElement | undefined; + + if (['ArrowDown', 'ArrowRight'].includes(ev.key)) { + ev.preventDefault(); + ev.stopPropagation(); + + nextEl = currentIndex === radios.length - 1 ? radios[0] : radios[currentIndex + 1]; + } else if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) { + ev.preventDefault(); + ev.stopPropagation(); + + nextEl = currentIndex === 0 ? radios[radios.length - 1] : radios[currentIndex - 1]; + } else if (ev.key === ' ' || ev.key === 'Enter') { + ev.preventDefault(); + ev.stopPropagation(); + + const allButtons = this.getButtons(); + const radioButtons = this.getRadioButtons(); + const buttonIndex = radioButtons.findIndex((b) => { + const buttonId = this.getButtonId(b, allButtons.indexOf(b)); + return buttonId === target.id; + }); + + if (buttonIndex !== -1) { + this.selectRadioButton(radioButtons[buttonIndex]); + this.buttonClick(radioButtons[buttonIndex]); + } + + return; + } + + // Focus the next radio button + if (nextEl) { + const allButtons = this.getButtons(); + const radioButtons = this.getRadioButtons(); + + const buttonIndex = radioButtons.findIndex((b) => { + const buttonId = this.getButtonId(b, allButtons.indexOf(b)); + return buttonId === nextEl?.id; + }); + + if (buttonIndex !== -1) { + this.selectRadioButton(radioButtons[buttonIndex]); + nextEl.focus(); + } + } + } + connectedCallback() { prepareOverlay(this.el); this.triggerChanged(); + + this.hasRadioButtons = this.getButtons().some((b) => b.role === 'radio'); } disconnectedCallback() { @@ -312,6 +455,8 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { if (!this.htmlAttributes?.id) { setOverlayId(this.el); } + // Initialize activeRadioId for radio buttons + this.buttonsChanged(); } componentDidLoad() { @@ -355,8 +500,83 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { this.triggerChanged(); } + private renderActionSheetButtons(filteredButtons: ActionSheetButton[]) { + const mode = getIonMode(this); + const { activeRadioId } = this; + console.log('Rendering buttons with activeRadioId:', activeRadioId); + + return filteredButtons.map((b, index) => { + const isRadio = b.role === 'radio'; + const buttonId = this.getButtonId(b, index); + const radioButtons = this.getRadioButtons(); + const isActiveRadio = isRadio && buttonId === activeRadioId; + const isFirstRadio = isRadio && b === radioButtons[0]; + + // For radio buttons, set tabindex: 0 for the active one, -1 for others + // For non-radio buttons, use default tabindex (undefined, which means 0) + + /** + * For radio buttons, set tabindex based on activeRadioId + * - If the button is the active radio, tabindex is 0 + * - If no radio is active, the first radio button should have tabindex 0 + * - All other radio buttons have tabindex -1 + * For non-radio buttons, use default tabindex (undefined, which means 0) + */ + let tabIndex: number | undefined; + + if (isRadio) { + // Focus on the active radio button + if (isActiveRadio) { + tabIndex = 0; + } else if (!activeRadioId && isFirstRadio) { + // No active radio, first radio gets focus + tabIndex = 0; + } else { + // All other radios are not focusable + tabIndex = -1; + } + } else { + tabIndex = undefined; + } + + // For radio buttons, set aria-checked based on activeRadioId + // Otherwise, use the value from htmlAttributes if provided + const htmlAttrs = { ...b.htmlAttributes }; + if (isRadio) { + htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false'; + } + + return ( + + ); + }); + } + render() { - const { header, htmlAttributes, overlayIndex } = this; + const { header, htmlAttributes, overlayIndex, activeRadioId, hasRadioButtons } = this; const mode = getIonMode(this); const allButtons = this.getButtons(); const cancelButton = allButtons.find((b) => b.role === 'cancel'); @@ -388,7 +608,11 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
(this.wrapperEl = el)}>
-
(this.groupEl = el)}> +
(this.groupEl = el)} + role={hasRadioButtons ? 'radiogroup' : undefined} + > {header !== undefined && (
{this.subHeader}
}
)} - {buttons.map((b) => ( - - ))} + {this.renderActionSheetButtons(buttons)}
{cancelButton && ( diff --git a/core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts b/core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts index abe16b55a75..0d6da17d321 100644 --- a/core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts +++ b/core/src/components/action-sheet/test/a11y/action-sheet.e2e.ts @@ -134,3 +134,58 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => { }); }); }); + +/** + * This behavior does not vary across modes/directions. + */ +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('action-sheet: radio buttons'), () => { + test('should render action sheet with radio buttons correctly', async ({ page }) => { + await page.goto(`/src/components/action-sheet/test/a11y`, config); + + const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent'); + const button = page.locator('#radioButtons'); + + await button.click(); + await ionActionSheetDidPresent.next(); + + const actionSheet = page.locator('ion-action-sheet'); + + const radioButtons = actionSheet.locator('.action-sheet-button[role="radio"]'); + await expect(radioButtons).toHaveCount(2); + }); + + test('should navigate radio buttons with keyboard', async ({ page, pageUtils }) => { + await page.goto(`/src/components/action-sheet/test/a11y`, config); + + const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent'); + const button = page.locator('#radioButtons'); + + await button.click(); + await ionActionSheetDidPresent.next(); + + // Focus on the radios + await pageUtils.pressKeys('Tab'); + + // Verify the first focusable radio button is focused + let focusedElement = await page.evaluate(() => document.activeElement?.textContent?.trim()); + expect(focusedElement).toBe('Option 2'); + + // Navigate to the next radio button + await page.keyboard.press('ArrowDown'); + + // Verify the first radio button is focused again (wrap around) + focusedElement = await page.evaluate(() => document.activeElement?.textContent?.trim()); + expect(focusedElement).toBe('Option 1'); + + // Navigate to the next radio button + await page.keyboard.press('ArrowDown'); + + // Navigate to the cancel button + await pageUtils.pressKeys('Tab'); + + focusedElement = await page.evaluate(() => document.activeElement?.textContent?.trim()); + expect(focusedElement).toBe('Cancel'); + }); + }); +}); diff --git a/core/src/components/action-sheet/test/a11y/index.html b/core/src/components/action-sheet/test/a11y/index.html index 8bb0a4ad9d7..a2e1b3b65a7 100644 --- a/core/src/components/action-sheet/test/a11y/index.html +++ b/core/src/components/action-sheet/test/a11y/index.html @@ -27,6 +27,7 @@

Action Sheet - A11y

+ diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 2b12082c505..39bc5f5c053 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -559,13 +559,15 @@ export class Select implements ComponentInterface { const isSelected = isOptionSelected(selectValue, value, this.compareWith); return { - role: isSelected ? 'selected' : '', + role: 'radio', text: option.textContent, cssClass: optClass, handler: () => { this.setValue(value); }, - ...(isSelected ? { htmlAttributes: { 'aria-description': 'selected' } } : {}), + htmlAttributes: { + 'aria-checked': isSelected ? 'true' : 'false', + }, } as ActionSheetButton; }); diff --git a/core/src/components/select/test/a11y/select.e2e.ts b/core/src/components/select/test/a11y/select.e2e.ts index 55416a982c4..bae522faf64 100644 --- a/core/src/components/select/test/a11y/select.e2e.ts +++ b/core/src/components/select/test/a11y/select.e2e.ts @@ -111,39 +111,3 @@ configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => { }); }); }); - -/** - * This behavior does not vary across modes/directions. - */ -configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { - test.describe(title('select: aria attributes'), () => { - test('should have a aria-description on the selected option when action sheet interface is open', async ({ - page, - }) => { - await page.setContent( - ` - - Apple - Banana - Orange - - `, - config - ); - - const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent'); - - const select = page.locator('ion-select'); - - await select.click(); - await ionActionSheetDidPresent.next(); - - const selectedOption = page.locator('.action-sheet-selected'); - await expect(selectedOption).toHaveAttribute('aria-description', 'selected'); - - // Check that the attribut is not added to non-selected option - const nonSelectedOption = page.locator('.select-interface-option:not(.action-sheet-selected)').first(); - await expect(nonSelectedOption).not.toHaveAttribute('aria-description'); - }); - }); -}); From 6aab5bdd4471511f5d3623566e43fddb55031814 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 12 Nov 2025 14:44:31 -0800 Subject: [PATCH 6/8] refactor(action-sheet): remove unused code --- core/src/components/action-sheet/action-sheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index 78a6637723c..8f8598e1da3 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -576,7 +576,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { } render() { - const { header, htmlAttributes, overlayIndex, activeRadioId, hasRadioButtons } = this; + const { header, htmlAttributes, overlayIndex, hasRadioButtons } = this; const mode = getIonMode(this); const allButtons = this.getButtons(); const cancelButton = allButtons.find((b) => b.role === 'cancel'); From 626370ba8047ad5ac3a40e62227e546993899081 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 14 Nov 2025 18:07:56 -0800 Subject: [PATCH 7/8] Update core/src/components/action-sheet/action-sheet.tsx Co-authored-by: Shane --- core/src/components/action-sheet/action-sheet.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index 8f8598e1da3..90b4d793494 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -503,7 +503,6 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { private renderActionSheetButtons(filteredButtons: ActionSheetButton[]) { const mode = getIonMode(this); const { activeRadioId } = this; - console.log('Rendering buttons with activeRadioId:', activeRadioId); return filteredButtons.map((b, index) => { const isRadio = b.role === 'radio'; From c150a614958c38fd4e041d9850eecae419ec0935 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 14 Nov 2025 18:16:54 -0800 Subject: [PATCH 8/8] refactor(action-sheet): update to accept dynamic roles --- core/src/components/action-sheet/action-sheet.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index 90b4d793494..2daae941e2d 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -90,12 +90,15 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { @Prop() buttons: (ActionSheetButton | string)[] = []; @Watch('buttons') buttonsChanged() { + const radioButtons = this.getRadioButtons(); + this.hasRadioButtons = radioButtons.length > 0; + // Initialize activeRadioId when buttons change if (this.hasRadioButtons) { - const allButtons = this.getButtons(); - const radioButtons = this.getRadioButtons(); const checkedButton = radioButtons.find((b) => b.htmlAttributes?.['aria-checked'] === 'true'); + if (checkedButton) { + const allButtons = this.getButtons(); const checkedIndex = allButtons.indexOf(checkedButton); this.activeRadioId = this.getButtonId(checkedButton, checkedIndex); } @@ -439,8 +442,6 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { connectedCallback() { prepareOverlay(this.el); this.triggerChanged(); - - this.hasRadioButtons = this.getButtons().some((b) => b.role === 'radio'); } disconnectedCallback() {