diff --git a/packages/main/src/RadioButton.hbs b/packages/main/src/RadioButton.hbs index f4146411cc1f..648452dbf44a 100644 --- a/packages/main/src/RadioButton.hbs +++ b/packages/main/src/RadioButton.hbs @@ -17,7 +17,7 @@ - + {{#if text}} diff --git a/packages/main/src/RadioButton.ts b/packages/main/src/RadioButton.ts index e09e3283d137..b6714d900f6f 100644 --- a/packages/main/src/RadioButton.ts +++ b/packages/main/src/RadioButton.ts @@ -102,8 +102,8 @@ class RadioButton extends UI5Element implements IFormElement { /** * Defines whether the component is read-only. * - * **Note:** A read-only component is not editable, - * but still provides visual feedback upon user interaction. + * **Note:** A read-only component isn't editable or selectable. + * However, because it's focusable, it still provides visual feedback upon user interaction. * @default false * @public */ @@ -125,6 +125,9 @@ class RadioButton extends UI5Element implements IFormElement { * **Note:** The property value can be changed with user interaction, * either by clicking/tapping on the component, * or by using the Space or Enter key. + * + * **Note:** Only enabled radio buttons can be checked. + * Read-only radio buttons are not selectable, and therefore are always unchecked. * @default false * @formEvents change * @formProperty @@ -429,7 +432,7 @@ class RadioButton extends UI5Element implements IFormElement { } get effectiveAriaDisabled() { - return this.disabled ? "true" : null; + return (this.disabled || this.readonly) ? "true" : null; } get ariaLabelText() { diff --git a/packages/main/src/RadioButtonGroup.ts b/packages/main/src/RadioButtonGroup.ts index 9f5171f5b7b5..40d0ef15a257 100644 --- a/packages/main/src/RadioButtonGroup.ts +++ b/packages/main/src/RadioButtonGroup.ts @@ -1,3 +1,4 @@ +import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; import type RadioButton from "./RadioButton.js"; class RadioButtonGroup { @@ -83,12 +84,12 @@ class RadioButtonGroup { return; } - const nextItemToSelect = this._nextSelectable(currentItemPosition, group); - if (!nextItemToSelect) { + const nextItemToFocus = this._nextFocusable(currentItemPosition, group); + if (!nextItemToFocus) { return; } - this.updateSelectionInGroup(nextItemToSelect, groupName); + this.updateSelectionInGroup(nextItemToFocus, groupName); } static updateFormValidity(groupName: string) { @@ -116,8 +117,21 @@ class RadioButtonGroup { const hasCheckedRadio = group.some(radioBtn => radioBtn.checked); group.filter(radioBtn => !radioBtn.disabled).forEach((radioBtn, idx) => { + let activeElement: Element | RadioButton = getActiveElement()!; + + if (activeElement.classList.contains("ui5-radio-root")) { + activeElement = activeElement.getRootNode() as Element; + if (activeElement instanceof ShadowRoot) { + activeElement = activeElement.host; + } + } + if (hasCheckedRadio) { - radioBtn._tabIndex = radioBtn.checked ? "0" : "-1"; + if (activeElement.hasAttribute("ui5-radio-button") && (activeElement as RadioButton).readonly) { + radioBtn._tabIndex = activeElement === radioBtn && radioBtn.readonly ? "0" : "-1"; + } else { + radioBtn._tabIndex = radioBtn.checked ? "0" : "-1"; + } } else { radioBtn._tabIndex = idx === 0 ? "0" : "-1"; } @@ -138,12 +152,12 @@ class RadioButtonGroup { return; } - const previousItemToSelect = this._previousSelectable(currentItemPosition, group); - if (!previousItemToSelect) { + const previousItemToFocus = this._previousFocusable(currentItemPosition, group); + if (!previousItemToFocus) { return; } - this.updateSelectionInGroup(previousItemToSelect, groupName); + this.updateSelectionInGroup(previousItemToFocus, groupName); } static selectItem(item: RadioButton, groupName: string) { @@ -154,12 +168,24 @@ class RadioButtonGroup { static updateSelectionInGroup(radioBtnToSelect: RadioButton, groupName: string) { const checkedRadio = this.getCheckedRadioFromGroup(groupName); - if (checkedRadio) { + if (checkedRadio && !radioBtnToSelect.readonly) { this._deselectRadio(checkedRadio); + this.checkedRadios.set(groupName, radioBtnToSelect); } - this._selectRadio(radioBtnToSelect); - this.checkedRadios.set(groupName, radioBtnToSelect); + // the focusable radio buttons are the enabled and the read-only ones, but only the enabled are selectable + if (radioBtnToSelect) { + radioBtnToSelect.focus(); + + if (!radioBtnToSelect.readonly) { + this._selectRadio(radioBtnToSelect); + } else { + // Ensure updateTabOrder is called after focus + setTimeout(() => { + this.updateTabOrder(groupName); + }, 0); + } + } } static _deselectRadio(radioBtn: RadioButton) { @@ -169,51 +195,48 @@ class RadioButtonGroup { } static _selectRadio(radioBtn: RadioButton) { - if (radioBtn) { - radioBtn.focus(); - radioBtn.checked = true; - radioBtn._checked = true; - radioBtn.fireEvent("change"); - } + radioBtn.checked = true; + radioBtn._checked = true; + radioBtn.fireEvent("change"); } - static _nextSelectable(pos: number, group: RadioButton[]): RadioButton | null { + static _nextFocusable(pos: number, group: RadioButton[]): RadioButton | null { if (!group) { return null; } const groupLength = group.length; - let nextRadioToSelect = null; + let nextRadioToFocus = null; if (pos === groupLength - 1) { - if (group[0].disabled || group[0].readonly) { - return this._nextSelectable(1, group); + if (group[0].disabled) { + return this._nextFocusable(1, group); } - nextRadioToSelect = group[0]; - } else if (group[pos + 1].disabled || group[pos + 1].readonly) { - return this._nextSelectable(pos + 1, group); + nextRadioToFocus = group[0]; + } else if (group[pos + 1].disabled) { + return this._nextFocusable(pos + 1, group); } else { - nextRadioToSelect = group[pos + 1]; + nextRadioToFocus = group[pos + 1]; } - return nextRadioToSelect; + return nextRadioToFocus; } - static _previousSelectable(pos: number, group: RadioButton[]): RadioButton | null { + static _previousFocusable(pos: number, group: RadioButton[]): RadioButton | null { const groupLength = group.length; - let previousRadioToSelect = null; + let previousRadioToFocus = null; if (pos === 0) { - if (group[groupLength - 1].disabled || group[groupLength - 1].readonly) { - return this._previousSelectable(groupLength - 1, group); + if (group[groupLength - 1].disabled) { + return this._previousFocusable(groupLength - 1, group); } - previousRadioToSelect = group[groupLength - 1]; - } else if (group[pos - 1].disabled || group[pos - 1].readonly) { - return this._previousSelectable(pos - 1, group); + previousRadioToFocus = group[groupLength - 1]; + } else if (group[pos - 1].disabled) { + return this._previousFocusable(pos - 1, group); } else { - previousRadioToSelect = group[pos - 1]; + previousRadioToFocus = group[pos - 1]; } - return previousRadioToSelect; + return previousRadioToFocus; } static enforceSingleSelection(radioBtn: RadioButton, groupName: string) { diff --git a/packages/main/test/pages/RadioButton.html b/packages/main/test/pages/RadioButton.html index e9a4e7e5dffa..39104e4b3cfe 100644 --- a/packages/main/test/pages/RadioButton.html +++ b/packages/main/test/pages/RadioButton.html @@ -27,6 +27,11 @@ + + + + +
diff --git a/packages/main/test/specs/RadioButton.spec.js b/packages/main/test/specs/RadioButton.spec.js index 3fa0fcaff677..65c5cdba8c6b 100644 --- a/packages/main/test/specs/RadioButton.spec.js +++ b/packages/main/test/specs/RadioButton.spec.js @@ -23,6 +23,7 @@ describe("Rendering", () => { assert.notOk(await readOnlyRadioButtonRoot.getAttribute("aria-readonly"), "aria-readonly is not set to the root level"); assert.strictEqual(await readOnlyRadioButtonInput.getAttribute("readonly"), "true", "internal input is readonly"); + assert.strictEqual(await readOnlyRadioButtonRoot.getAttribute("aria-disabled"), "true", "aria-disabled = true"); }); }); @@ -87,6 +88,7 @@ describe("RadioButton general interaction", () => { const field = await browser.$("#tabField"); const radioButtonPreviouslySelected = await browser.$("#groupRb1"); const radioButtonToBeSelected = await browser.$("#groupRb3"); + const radioButtonToBeSelectedNext = await browser.$("#groupRbReadOnly"); await field.click(); await field.keys("Tab"); @@ -96,7 +98,14 @@ describe("RadioButton general interaction", () => { assert.notOk(await radioButtonPreviouslySelected.getProperty("checked"), "Previously selected item has been de-selected."); assert.ok(await radioButtonToBeSelected.getProperty("checked"), "Pressing ArrowRight selects the next (not disabled) radio in the group."); - await radioButtonToBeSelected.keys("Tab"); + await radioButtonToBeSelected.keys("ArrowRight"); + const activeElementId = await browser.$(await browser.getActiveElement()).getAttribute("id"); + + assert.ok(await radioButtonToBeSelected.getProperty("checked"), "Previously selected item is still selected"); + assert.notOk(await radioButtonToBeSelectedNext.getProperty("checked"), "Read-only radio button is not selected."); + assert.ok(activeElementId === "groupRbReadOnly", " Focus is on the last radio button, which is read-only"); + + await radioButtonToBeSelectedNext.keys("Tab"); }); it("tests radio buttons selection within group with ARROW-LEFT key", async () => {