diff --git a/packages/main/src/RadioButton.hbs b/packages/main/src/RadioButton.hbs index a3f4fc6d7402..1be1cbd9b064 100644 --- a/packages/main/src/RadioButton.hbs +++ b/packages/main/src/RadioButton.hbs @@ -11,7 +11,7 @@ - + {{#if ctr._label.text}} diff --git a/packages/main/src/RadioButton.js b/packages/main/src/RadioButton.js index de9869d8633a..baff873cd55a 100644 --- a/packages/main/src/RadioButton.js +++ b/packages/main/src/RadioButton.js @@ -92,12 +92,17 @@ const metadata = { }, /** - * Defines the group to which the ui5-radiobutton belongs. + * Defines the name of the ui5-radiobutton. + * Radio buttons with the same name will form a radio button group. + *
Note: + * The selection can be changed with ARROW_UP/DOWN and ARROW_LEFT/RIGHT keys between radios in same group. + *
Note: + * Only one radio button can be selected per group. * * @type {string} * @public */ - group: { + name: { defaultValue: "", type: String, }, @@ -127,8 +132,8 @@ const metadata = { * When a ui5-radiobutton is selected by the user, the * select event is fired. * When a ui5-radiobutton that is within a group is selected, the one - * that was previously selected gets - * automatically deselected. + * that was previously selected gets automatically deselected. You can group radio buttons by using the name property. + * * *

ES6 Module Import

* @@ -170,24 +175,24 @@ class RadioButton extends UI5Element { } syncGroup() { - const oldGroup = this._group; - const currentGroup = this.group; - - if (currentGroup === oldGroup) { - return; - } - - if (oldGroup) { - // remove the control from the previous group - RadioButtonGroup.removeFromGroup(this, oldGroup); - } - - if (currentGroup) { - // add the control to the existing group - RadioButtonGroup.addToGroup(this, currentGroup); + const oldGroup = this._name; + const currentGroup = this.name; + + if (currentGroup !== oldGroup) { + if (oldGroup) { + // remove the control from the previous group + RadioButtonGroup.removeFromGroup(this, oldGroup); + } + + if (currentGroup) { + // add the control to the existing group + RadioButtonGroup.addToGroup(this, currentGroup); + } + } else if (currentGroup) { + RadioButtonGroup.enforceSingleSelection(this, currentGroup); } - this._group = this.group; + this._name = this.name; } onclick() { @@ -195,7 +200,7 @@ class RadioButton extends UI5Element { } _handleDown(event) { - const currentGroup = this.group; + const currentGroup = this.name; if (!currentGroup) { return; @@ -206,7 +211,7 @@ class RadioButton extends UI5Element { } _handleUp(event) { - const currentGroup = this.group; + const currentGroup = this.name; if (!currentGroup) { return; @@ -245,13 +250,13 @@ class RadioButton extends UI5Element { return this; } - if (!this.group) { + if (!this.name) { this.selected = !this.selected; this.fireEvent("select"); return this; } - RadioButtonGroup.selectItem(this, this.group); + RadioButtonGroup.selectItem(this, this.name); return this; } diff --git a/packages/main/src/RadioButtonGroup.js b/packages/main/src/RadioButtonGroup.js index ebfddfdb16a1..2efb8f55197b 100644 --- a/packages/main/src/RadioButtonGroup.js +++ b/packages/main/src/RadioButtonGroup.js @@ -7,42 +7,55 @@ class RadioButtonGroup { return this.groups.get(groupName); } + static getSelectedRadioFromGroup(groupName) { + return this.selectedRadios.get(groupName); + } + static removeGroup(groupName) { + this.selectedRadios.delete(groupName); return this.groups.delete(groupName); } - static addToGroup(control, groupName) { + static addToGroup(radioBtn, groupName) { if (this.hasGroup(groupName)) { - this.getGroup(groupName).push(control); + this.enforceSingleSelection(radioBtn, groupName); + this.getGroup(groupName).push(radioBtn); } else { - this.createGroup(control, groupName); + this.createGroup(radioBtn, groupName); } } - static removeFromGroup(control, groupName) { + static removeFromGroup(radioBtn, groupName) { if (!this.hasGroup(groupName)) { return; } const group = this.getGroup(groupName); + const selectedRadio = this.getSelectedRadioFromGroup(groupName); - // Remove the control from the given group - group.forEach((_control, idx, arr) => { - if (control._id === _control._id) { + // Remove the radio button from the given group + group.forEach((_radioBtn, idx, arr) => { + if (radioBtn._id === _radioBtn._id) { return arr.splice(idx, 1); } }); + if (selectedRadio === radioBtn) { + this.selectedRadios.set(groupName, null); + } + // Remove the group if it is empty if (!group.length) { this.removeGroup(groupName); } } - static createGroup(control, groupName) { - if (!this.hasGroup(groupName)) { - this.groups.set(groupName, [control]); + static createGroup(radioBtn, groupName) { + if (radioBtn.selected) { + this.selectedRadios.set(groupName, radioBtn); } + + this.groups.set(groupName, [radioBtn]); } static selectNextItem(item, groupName) { @@ -56,16 +69,7 @@ class RadioButtonGroup { const nextItemToSelect = this._nextSelectable(currentItemPosition, group); - // de-select all the rest - group.forEach(radio => { - if (radio._id !== nextItemToSelect._id) { - radio.selected = false; - radio.fireEvent("select", { selected: radio.selected }); - } - }); - - // select the next item - this._selectItem(nextItemToSelect); + this.updateSelectionInGroup(nextItemToSelect, groupName); } static selectPreviousItem(item, groupName) { @@ -79,81 +83,103 @@ class RadioButtonGroup { const previousItemToSelect = this._previousSelectable(currentItemPosition, group); - // de-select all the rest - group.forEach(radio => { - if (radio._id !== previousItemToSelect._id) { - radio.selected = false; - radio.fireEvent("select", { selected: radio.selected }); - } - }); - - // select the next item - this._selectItem(previousItemToSelect); + this.updateSelectionInGroup(previousItemToSelect, groupName); } static selectItem(item, groupName) { - const group = this.getGroup(groupName); + this.updateSelectionInGroup(item, groupName); + } - // de-select all the rest - group.forEach(radio => { - if (radio._id !== item._id) { - radio.selected = false; - radio.fireEvent("select", { selected: radio.selected }); - } - }); + static updateSelectionInGroup(radioBtnToSelect, groupName) { + const selectedRadio = this.getSelectedRadioFromGroup(groupName); - this._selectItem(item); + this._deselectRadio(selectedRadio); + this._selectRadio(radioBtnToSelect); + this.selectedRadios.set(groupName, radioBtnToSelect); } - static get groups() { - if (!this._groups) { - this._groups = new Map(); + static _deselectRadio(radioBtn) { + if (radioBtn) { + radioBtn.selected = false; } - return this._groups; } - static _selectItem(item) { - item.focus(); - item.selected = true; - item.fireEvent("select", { selected: item.selected }); + static _selectRadio(radioBtn) { + if (radioBtn) { + radioBtn.focus(); + radioBtn.selected = true; + radioBtn._selected = true; + radioBtn.fireEvent("select"); + } } static _nextSelectable(pos, group) { const groupLength = group.length; - let nextItemToSelect = null; + let nextRadioToSelect = null; if (pos === groupLength - 1) { if (!group[0].disabled) { - nextItemToSelect = group[0]; + nextRadioToSelect = group[0]; } else { return this._nextSelectable(0, group); } } else if (!group[++pos].disabled) { - nextItemToSelect = group[pos]; + nextRadioToSelect = group[pos]; } else { return this._nextSelectable(pos, group); } - return nextItemToSelect; + return nextRadioToSelect; } static _previousSelectable(pos, group) { const groupLength = group.length; - let previousSelectable = null; + let previousRadioToSelect = null; if (pos === 0) { if (!group[groupLength - 1].disabled) { - previousSelectable = group[groupLength - 1]; + previousRadioToSelect = group[groupLength - 1]; } else { return this._previousSelectable(groupLength - 1, group); } } else if (!group[--pos].disabled) { - previousSelectable = group[pos]; + previousRadioToSelect = group[pos]; } else { return this._previousSelectable(pos, group); } - return previousSelectable; + return previousRadioToSelect; + } + + static enforceSingleSelection(radioBtn, groupName) { + const selectedRadio = this.getSelectedRadioFromGroup(groupName); + + if (!selectedRadio) { + return; + } + + if (radioBtn.selected) { + if (radioBtn !== selectedRadio) { + this._deselectRadio(selectedRadio); + this.selectedRadios.set(groupName, radioBtn); + } + } else if (radioBtn === selectedRadio) { + this.selectedRadios.set(groupName, null); + } + } + + static get groups() { + if (!this._groups) { + this._groups = new Map(); + } + return this._groups; + } + + static get selectedRadios() { + if (!this._selectedRadios) { + this._selectedRadios = new Map(); + } + return this._selectedRadios; } } diff --git a/packages/main/src/RadioButtonTemplateContext.js b/packages/main/src/RadioButtonTemplateContext.js index 629865cc19f6..1fc1a3faef1e 100644 --- a/packages/main/src/RadioButtonTemplateContext.js +++ b/packages/main/src/RadioButtonTemplateContext.js @@ -25,7 +25,7 @@ class RadioButtonTemplateContext { context = { ctr: state, readOnly: state.disabled || state.readOnly, - tabIndex: state.disabled || (!state.selected && state.group) ? "-1" : "0", + tabIndex: state.disabled || (!state.selected && state.name) ? "-1" : "0", circle: compact ? SVGConfig.compact : SVGConfig.default, classes: { main: mainClasses, inner: innerClasses }, styles: { diff --git a/packages/main/test/sap/ui/webcomponents/main/pages/RadioButton.html b/packages/main/test/sap/ui/webcomponents/main/pages/RadioButton.html index 15076f891848..a06f20e835af 100644 --- a/packages/main/test/sap/ui/webcomponents/main/pages/RadioButton.html +++ b/packages/main/test/sap/ui/webcomponents/main/pages/RadioButton.html @@ -34,18 +34,29 @@

ui5-radiobutton

ui5-radiobutton in group - - - + + +
ui5-radiobutton in group - - - + + +
+
+ Group of states + + + + + + + +
+
ui5-radiobutton states

@@ -69,6 +80,7 @@

ui5-radiobutton

\ No newline at end of file diff --git a/packages/main/test/sap/ui/webcomponents/main/qunit/RadioButton.qunit.js b/packages/main/test/sap/ui/webcomponents/main/qunit/RadioButton.qunit.js index 04a3e26f3e91..bef7ac25b9f5 100644 --- a/packages/main/test/sap/ui/webcomponents/main/qunit/RadioButton.qunit.js +++ b/packages/main/test/sap/ui/webcomponents/main/qunit/RadioButton.qunit.js @@ -56,7 +56,7 @@ TestHelper.ready(function () { assert.notOk(radiobutton.querySelector("ui5-label"), "the default text is empty and ui5-label is not rendered."); }); - QUnit.test("The 'group' is not set by default", function (assert) { + QUnit.test("The 'name' is not set by default", function (assert) { var radiobutton = this.getRadioButtonRoot(); assert.notOk(radiobutton.querySelector("input").getAttribute("name"), "there is no name attribute"); @@ -157,27 +157,27 @@ TestHelper.ready(function () { }); }); - QUnit.test("changing the 'group' is reflected in the DOM", function (assert) { + QUnit.test("changing the 'name' is reflected in the DOM", function (assert) { assert.expect(1); var done = assert.async(), - expectedGroup = "test", + expectedName = "test", radiobutton = this.getRadioButtonRoot(); - this.radiobutton.setAttribute("group", expectedGroup); + this.radiobutton.setAttribute("name", expectedName); RenderScheduler.whenFinished().then(function () { - assert.equal(radiobutton.querySelector("input").getAttribute("name"), expectedGroup, "the name is set to " + expectedGroup); + assert.equal(radiobutton.querySelector("input").getAttribute("name"), expectedName, "the name is set to " + expectedName); done(); }); }); }); - QUnit.module("Group", function (hooks) { + QUnit.module("Name", function (hooks) { hooks.beforeEach(function () { - var html = '' - + '' - + ''; + var html = '' + + '' + + ''; fixture.innerHTML = html; diff --git a/packages/main/test/sap/ui/webcomponents/main/samples/RadioButton.sample.html b/packages/main/test/sap/ui/webcomponents/main/samples/RadioButton.sample.html index efd6f88091ee..db293fbe71d5 100644 --- a/packages/main/test/sap/ui/webcomponents/main/samples/RadioButton.sample.html +++ b/packages/main/test/sap/ui/webcomponents/main/samples/RadioButton.sample.html @@ -40,48 +40,58 @@

RadioButton

Basic RadioButton Types

- - - - - + + + + +

-<ui5-radiobutton text="Option A" selected group="GroupA"></ui5-radiobutton>
-<ui5-radiobutton text="Option B" value-state="None" group="GroupA"></ui5-radiobutton>
-<ui5-radiobutton text="Option C" value-state="Warning" group="GroupA"></ui5-radiobutton>
-<ui5-radiobutton text="Option D" value-state="Error" group="GroupA"></ui5-radiobutton>
-<ui5-radiobutton text="Option E" disabled group="GroupA"></ui5-radiobutton>
+<ui5-radiobutton text="Option A" selected name="GroupA"></ui5-radiobutton>
+<ui5-radiobutton text="Option B" value-state="None" name="GroupA"></ui5-radiobutton>
+<ui5-radiobutton text="Option C" value-state="Warning" name="GroupA"></ui5-radiobutton>
+<ui5-radiobutton text="Option D" value-state="Error" name="GroupA"></ui5-radiobutton>
+<ui5-radiobutton text="Option E" disabled name="GroupA"></ui5-radiobutton>
 		
-

Grouped RadioButton (navigate with UP and DOWN arrow keys)

+

RadioButton in group - navigate via [UP/Right] and [DOWN/Left] arrow keys

-
- First Group - - - +
+ Group of states + Selected radio: None + + +
-
- Second Group - - - +
+ Group of options + Selected radio: Option A + + +

-<div class="radio-button-group">
-	<ui5-title>First Group</ui5-title>
-	<ui5-radiobutton text="None" value-state="None" selected group="GroupB"></ui5-radiobutton>
-	<ui5-radiobutton text="Warning" value-state="Warning" group="GroupB"></ui5-radiobutton>
-	<ui5-radiobutton text="Error" value-state="Error" group="GroupB"></ui5-radiobutton>
+<script>
+radioGroup.addEventListener("select", function(e) {
+	lblRadioGroup.innerHTML = e.target.text;
+});
+</script>
+
+<div id="radioGroup" class="radio-button-group">
+	<ui5-title>Group of states</ui5-title>
+	<ui5-label id="lblRadioGroup">Selected radio: None</ui5-label>
+	<ui5-radiobutton text="None" value-state="None" selected name="GroupB"></ui5-radiobutton>
+	<ui5-radiobutton text="Warning" value-state="Warning" name="GroupB"></ui5-radiobutton>
+	<ui5-radiobutton text="Error" value-state="Error" name="GroupB"></ui5-radiobutton>
 </div>
-<div class="radio-button-group">
-	<ui5-title>Second Group</ui5-title>
-	<ui5-radiobutton text="Option A" selected group="GroupC"></ui5-radiobutton>
-	<ui5-radiobutton text="Option B" value-state="None" group="GroupC"></ui5-radiobutton>
-	<ui5-radiobutton text="Option C" value-state="None" group="GroupC"></ui5-radiobutton>
+<div id="radioGroup2" class="radio-button-group">
+	<ui5-title>Group of options</ui5-title>
+	<ui5-label id="lblRadioGroup2">Selected radio: Option A</ui5-label>
+	<ui5-radiobutton text="Option A" selected name="GroupC"></ui5-radiobutton>
+	<ui5-radiobutton text="Option B" value-state="None" name="GroupC"></ui5-radiobutton>
+	<ui5-radiobutton text="Option C" value-state="None" name="GroupC"></ui5-radiobutton>
 </div>
 		
@@ -90,6 +100,16 @@

Grouped RadioButton (navigate with UP and DOWN arrow keys)

window.prettyPrint(); + + diff --git a/packages/main/test/specs/RadioButton.spec.js b/packages/main/test/specs/RadioButton.spec.js index 92c31cff888e..4c8bc4fa5488 100644 --- a/packages/main/test/specs/RadioButton.spec.js +++ b/packages/main/test/specs/RadioButton.spec.js @@ -4,8 +4,8 @@ describe("RadioButton general interaction", () => { browser.url("http://localhost:8080/test-resources/sap/ui/webcomponents/main/pages/RadioButton.html"); it("tests select event", () => { - const radioButton = browser.findElementDeep("#rb1"); - const field = browser.findElementDeep("#field"); + const radioButton = browser.$("#rb1"); + const field = browser.$("#field"); radioButton.click(); assert.strictEqual(field.getProperty("value"), "1", "Select event should be fired 1 time."); @@ -15,9 +15,9 @@ describe("RadioButton general interaction", () => { }); it("tests select event upon ENTER", () => { - const radioButton1 = browser.findElementDeep("#rb1"); - const radioButton2 = browser.findElementDeep("#rb2"); - const field = browser.findElementDeep("#field"); + const radioButton1 = browser.$("#rb1"); + const radioButton2 = browser.$("#rb2"); + const field = browser.$("#field"); radioButton1.click(); radioButton1.keys("Tab"); @@ -30,9 +30,9 @@ describe("RadioButton general interaction", () => { }); it("tests select event upon SPACE", () => { - const radioButton1 = browser.findElementDeep("#rb2"); - const radioButton2 = browser.findElementDeep("#rb3"); - const field = browser.findElementDeep("#field"); + const radioButton1 = browser.$("#rb2"); + const radioButton2 = browser.$("#rb3"); + const field = browser.$("#field"); radioButton1.click(); radioButton1.keys("Tab"); @@ -45,8 +45,8 @@ describe("RadioButton general interaction", () => { }); it("tests change event not fired, when disabled", () => { - const radioButton = browser.findElementDeep("#rb4"); - const field = browser.findElementDeep("#field"); + const radioButton = browser.$("#rb4"); + const field = browser.$("#field"); radioButton.click(); radioButton.keys("Space"); @@ -56,9 +56,9 @@ describe("RadioButton general interaction", () => { }); it("tests radio buttons selection within group with AROW-RIGHT key", () => { - const field = browser.findElementDeep("#field"); - const radioButtonPreviouslySelected = browser.findElementDeep("#groupRb1"); - const radioButtonToBeSelected = browser.findElementDeep("#groupRb3"); + const field = browser.$("#field"); + const radioButtonPreviouslySelected = browser.$("#groupRb1"); + const radioButtonToBeSelected = browser.$("#groupRb3"); field.click(); field.keys("Tab"); @@ -72,8 +72,8 @@ describe("RadioButton general interaction", () => { }); it("tests radio buttons selection within group with AROW-LEFT key", () => { - const radioButtonPreviouslySelected = browser.findElementDeep("#groupRb4"); - const radioButtonToBeSelected = browser.findElementDeep("#groupRb6"); + const radioButtonPreviouslySelected = browser.$("#groupRb4"); + const radioButtonToBeSelected = browser.$("#groupRb6"); radioButtonPreviouslySelected.keys("ArrowLeft"); @@ -82,12 +82,35 @@ describe("RadioButton general interaction", () => { }); it("tests radio buttons selection within group by clicking", () => { - const radioButtonPreviouslySelected = browser.findElementDeep("#groupRb6"); - const radioButtonToBeSelected = browser.findElementDeep("#groupRb4"); + const radioButtonPreviouslySelected = browser.$("#groupRb6"); + const radioButtonToBeSelected = browser.$("#groupRb4"); radioButtonToBeSelected.click(); assert.ok(!radioButtonPreviouslySelected.getProperty("selected"), "Previously selected item has been de-selected."); assert.ok(radioButtonToBeSelected.getProperty("selected"), "Pressing ArrowRight selects the next (not disabled) radio in the group."); }); + + it("tests single selection within group, even if multiple radios are set as selected", () => { + // radio with property selected=true, but not selected + const radioButtonNotSelected = browser.findElementDeep("#groupRb7 >>> .sapMRb"); + + // radio with property selected=true and actually selected as subsequent + const radioButtonActuallySelected = browser.findElementDeep("#groupRb10 >>> .sapMRb"); + + assert.ok(!radioButtonNotSelected.hasClass("sapMRbSel"), "The radio is not selected as following one selected"); + assert.ok(radioButtonActuallySelected.hasClass("sapMRbSel"), 'The correct radio is selected'); + }); + + it("tests select event from radio buttons within group", () => { + const radioButtonToBeSelected = browser.$("#groupRb7"); + const lblEventCounter = browser.$("#lblEventCounter"); + const lblSelectedRadio = browser.$("#lblRadioGroup"); + + radioButtonToBeSelected.click(); + + assert.equal(lblEventCounter.getHTML(false), "1", 'The select event is fired once'); + assert.equal(lblSelectedRadio.getHTML(false), radioButtonToBeSelected.getProperty("text"), "The correct radio is selected"); + }); + }); \ No newline at end of file