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 () => {