Skip to content

Commit 59497ee

Browse files
authored
fix(ui5-select): correct role and screen reader speech out (#2587)
- Role of the ui5-select is changed to combobox which change the speech output near to the default behavior. - The issue where role="button" can't be used with aria-required / required attribute is fixed. - Overwriting the aria-label speech output with aria-labelledby is fixed. Fixes: #2485 Fixes: #2339 Fixes: #2142
1 parent d9e3b0e commit 59497ee

File tree

4 files changed

+90
-37
lines changed

4 files changed

+90
-37
lines changed

packages/main/src/Select.hbs

+17-15
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
<div
22
class="ui5-select-root"
33
dir="{{effectiveDir}}"
4-
tabindex="{{tabIndex}}"
54
id="{{_id}}-select"
6-
role="button"
7-
aria-haspopup="listbox"
8-
aria-label="{{ariaLabelText}}"
9-
aria-labelledby="{{_id}}-label"
10-
aria-describedby="{{valueStateTextId}}"
11-
aria-disabled="{{isDisabled}}"
12-
aria-required="{{required}}"
13-
aria-expanded="{{_isPickerOpen}}"
14-
@keydown="{{_onkeydown}}"
15-
@keyup="{{_onkeyup}}"
16-
@focusin="{{_onfocusin}}"
17-
@focusout="{{_onfocusout}}"
185
@click="{{_toggleRespPopover}}"
196
>
20-
<div class="ui5-select-label-root">
21-
<ui5-label id="{{_id}}-label" for="{{_id}}-select">{{_text}}</ui5-label>
7+
<div class="ui5-select-label-root"
8+
tabindex="{{tabIndex}}"
9+
role="combobox"
10+
aria-haspopup="listbox"
11+
aria-label="{{ariaLabelText}}"
12+
aria-describedby="{{valueStateTextId}}"
13+
aria-disabled="{{isDisabled}}"
14+
aria-required="{{required}}"
15+
aria-expanded="{{_isPickerOpen}}"
16+
@keydown="{{_onkeydown}}"
17+
@keyup="{{_onkeyup}}"
18+
@focusin="{{_onfocusin}}"
19+
@focusout="{{_onfocusout}}"
20+
>
21+
{{_text}}
2222
</div>
2323

24+
<span id="{{_id}}-selectionText" class="ui5-hidden-text" aria-live="polite" role="status"></span>
25+
2426
<ui5-icon
2527
name="slim-arrow-down"
2628
input-icon

packages/main/src/Select.js

+16
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ class Select extends UI5Element {
320320

321321
_onfocusout() {
322322
this.focused = false;
323+
this.itemSelectionAnnounce();
323324
}
324325

325326
get _isPickerOpen() {
@@ -488,6 +489,7 @@ class Select extends UI5Element {
488489

489490
_handleArrowNavigation(event, shouldFireEvent) {
490491
let nextIndex = -1;
492+
const currentIndex = this._selectedIndex;
491493
const isDownKey = isDown(event);
492494
const isUpKey = isUp(event);
493495

@@ -503,6 +505,10 @@ class Select extends UI5Element {
503505
this.options[nextIndex].selected = true;
504506
this._selectedIndex = nextIndex === -1 ? this._selectedIndex : nextIndex;
505507

508+
if (currentIndex !== this._selectedIndex) {
509+
this.itemSelectionAnnounce();
510+
}
511+
506512
if (shouldFireEvent) {
507513
this._fireChangeEvent(this.options[nextIndex]);
508514
}
@@ -637,6 +643,16 @@ class Select extends UI5Element {
637643
return isPhone();
638644
}
639645

646+
itemSelectionAnnounce() {
647+
const invisibleText = this.shadowRoot.querySelector(`#${this._id}-selectionText`);
648+
649+
if (this.focused && !this._isPickerOpen && this._currentlySelectedOption) {
650+
invisibleText.textContent = this._currentlySelectedOption.textContent;
651+
} else {
652+
invisibleText.textContent = "";
653+
}
654+
}
655+
640656
async openValueStatePopover() {
641657
this.popover = await this._getPopover();
642658
if (this.popover) {

packages/main/src/themes/Select.css

+9-6
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@
1111
}
1212

1313
.ui5-select-label-root {
14-
display: inline-flex;
15-
align-items: center;
1614
flex-shrink: 1;
1715
flex-grow: 1;
18-
height: 100%;
16+
align-self: center;
1917
min-width: 1rem;
2018
padding-left: 0.5rem;
21-
}
22-
23-
.ui5-select-label-root [ui5-label] {
2419
cursor: pointer;
20+
outline: none;
21+
white-space: nowrap;
22+
overflow: hidden;
23+
text-overflow: ellipsis;
24+
color: var(--sapContent_LabelColor);
25+
font-family: "72override", var(--sapFontFamily);
26+
font-size: var(--sapFontSize);
27+
font-weight: normal;
2528
}
2629

2730
:host [dir="rtl"].ui5-select-root .ui5-select-label-root {

packages/main/test/specs/Select.spec.js

+48-16
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe("Select general interaction", () => {
55

66
it("fires change on selection", () => {
77
const select = $("#mySelect");
8-
const selectText = browser.$("#mySelect").shadow$("ui5-label");
8+
const selectText = browser.$("#mySelect").shadow$(".ui5-select-label-root");
99
const inputResult = browser.$("#inputResult").shadow$("input");
1010
const EXPECTED_SELECTION_TEXT = "Cozy";
1111

@@ -34,7 +34,7 @@ describe("Select general interaction", () => {
3434

3535
it("fires change on selection with keyboard handling", () => {
3636
const select = $("#mySelect2").shadow$(".ui5-select-root");
37-
const selectText = browser.$("#mySelect2").shadow$("ui5-label");
37+
const selectText = browser.$("#mySelect2").shadow$(".ui5-select-label-root");
3838
const inputResult = browser.$("#inputResult");
3939
const EXPECTED_SELECTION_TEXT1 = "Compact";
4040
const EXPECTED_SELECTION_TEXT2 = "Condensed";
@@ -57,7 +57,7 @@ describe("Select general interaction", () => {
5757
it("changes selection while closed with Arrow Up/Down", () => {
5858
const inputResult = browser.$("#inputResult").shadow$("input");
5959
const select = $("#mySelect2");
60-
const selectText = browser.$("#mySelect2").shadow$("ui5-label");
60+
const selectText = browser.$("#mySelect2").shadow$(".ui5-select-label-root");
6161
const EXPECTED_SELECTION_TEXT1 = "Compact";
6262
const EXPECTED_SELECTION_TEXT2 = "Condensed";
6363

@@ -74,6 +74,39 @@ describe("Select general interaction", () => {
7474
assert.strictEqual(inputResult.getProperty("value"), "5", "Change event should have fired twice");
7575
});
7676

77+
it("changes selection sync with selection announcement", () => {
78+
const btn = $("#myBtn2");
79+
const inputResult = browser.$("#inputResult").shadow$("input");
80+
const select = $("#mySelect2");
81+
const selectId = select.getProperty("_id")
82+
const selectText = browser.$("#mySelect2").shadow$(".ui5-select-label-root");
83+
const selectionText = browser.$("#mySelect2").shadow$(`#${selectId}-selectionText`);
84+
const EXPECTED_SELECTION_TEXT1 = "Compact";
85+
const EXPECTED_SELECTION_TEXT2 = "Condensed";
86+
87+
select.click();
88+
select.keys("Escape");
89+
90+
assert.strictEqual(selectionText.getHTML(false), "", "Selection announcement text should be clear if there is no interaction");
91+
92+
select.keys("ArrowUp");
93+
assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT1), "Arrow Up should change selected item");
94+
assert.strictEqual(selectionText.getHTML(false), EXPECTED_SELECTION_TEXT1, "Selection announcement text should be equalt to the current selected item's text");
95+
96+
select.click();
97+
select.keys("Escape");
98+
assert.strictEqual(selectionText.getHTML(false), "", "Selection announcement text should be cleared if the picker is opened");
99+
100+
select.keys("ArrowDown");
101+
assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT2), "Arrow Up should change selected item");
102+
assert.strictEqual(selectionText.getHTML(false), EXPECTED_SELECTION_TEXT2, "Selection announcement text should be equalt to the current selected item's text");
103+
104+
btn.click();
105+
assert.strictEqual(selectionText.getHTML(false), "", "Selection announcement text should be cleared on focusout");
106+
107+
assert.strictEqual(inputResult.getProperty("value"), "7", "Change event should have fired twice");
108+
});
109+
77110
it("changes selection on Tab", () => {
78111
const select = browser.$("#keyboardHandling");
79112
const EXPECTED_SELECTION_TEXT = "Banana";
@@ -84,7 +117,7 @@ describe("Select general interaction", () => {
84117

85118
select.keys("ArrowUp");
86119
select.keys("Tab");
87-
const selectText = select.shadow$("ui5-label");
120+
const selectText = select.shadow$(".ui5-select-label-root");
88121

89122
assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT) > -1, "Arrow Up should change selected item");
90123

@@ -105,7 +138,7 @@ describe("Select general interaction", () => {
105138

106139
select.keys("ArrowDown");
107140
browser.keys(["Shift", "Tab"]);
108-
const selectText = select.shadow$("ui5-label");
141+
const selectText = select.shadow$(".ui5-select-label-root");
109142

110143
assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT) > -1, "Arrow Down should change selected item");
111144

@@ -119,7 +152,7 @@ describe("Select general interaction", () => {
119152
it("tests selection does not cycle with ArrowDown", () => {
120153
const select = $("#selectionNotCycling");
121154
const EXPECTED_SELECTION_TEXT = "Opt3";
122-
const selectOptionText = select.shadow$("ui5-label");
155+
const selectOptionText = select.shadow$(".ui5-select-label-root");
123156

124157
select.click();
125158
assert.ok(selectOptionText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT) > -1, "Selected option text is " + EXPECTED_SELECTION_TEXT);
@@ -135,7 +168,7 @@ describe("Select general interaction", () => {
135168
it("tests selection does not cycle with ArrowUp", () => {
136169
const select = $("#selectionNotCycling2");
137170
const EXPECTED_SELECTION_TEXT = "Opt1";
138-
const selectOptionText = select.shadow$("ui5-label");
171+
const selectOptionText = select.shadow$(".ui5-select-label-root");
139172

140173
select.click();
141174
assert.ok(selectOptionText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT) > -1, "Selected option text is " + EXPECTED_SELECTION_TEXT);
@@ -223,18 +256,18 @@ describe("Select general interaction", () => {
223256

224257
it("reverts value before open after clicking on escape", () => {
225258
const select = $("#mySelect");
226-
const selectText = browser.$("#mySelect").shadow$("ui5-label").getHTML(false);
259+
const selectText = browser.$("#mySelect").shadow$(".ui5-select-label-root").getHTML(false);
227260
const inputResult = browser.$("#inputResult").shadow$("input");
228261

229262
select.click();
230263
select.keys("ArrowDown");
231264
select.keys("Escape");
232265

233266
const selectedOption = browser.$("#mySelect ui5-option[selected]");
234-
const selectTextAfterEscape = browser.$("#mySelect").shadow$("ui5-label").getHTML(false);
267+
const selectTextAfterEscape = browser.$("#mySelect").shadow$(".ui5-select-label-root").getHTML(false);
235268

236269
assert.ok(selectedOption.getProperty("selected"), "Initially selected item should remain selected");
237-
assert.strictEqual(inputResult.getProperty("value"), "5", "Change event should not be fired");
270+
assert.strictEqual(inputResult.getProperty("value"), "7", "Change event should not be fired");
238271
assert.strictEqual(selectTextAfterEscape, selectText, "Initially selected item should remain selected");
239272
});
240273

@@ -250,7 +283,7 @@ describe("Select general interaction", () => {
250283
// focus out select
251284
btn.click();
252285

253-
assert.strictEqual(inputResult.getProperty("value"), "6", "Change event should be fired");
286+
assert.strictEqual(inputResult.getProperty("value"), "8", "Change event should be fired");
254287
});
255288

256289
it("fires change event after selecting a previewed item", () => {
@@ -268,12 +301,12 @@ describe("Select general interaction", () => {
268301

269302
firstItem.click();
270303

271-
assert.strictEqual(inputResult.getProperty("value"), "7", "Change event should be fired");
304+
assert.strictEqual(inputResult.getProperty("value"), "9", "Change event should be fired");
272305
});
273306

274307
it("tests ESC on closed picker", () => {
275308
const select = $("#mySelectEsc");
276-
const selectText = browser.$("#mySelectEsc").shadow$("ui5-label");
309+
const selectText = browser.$("#mySelectEsc").shadow$(".ui5-select-label-root");
277310
const EXPECTED_SELECTION_TEXT = "Cozy";
278311
const EXPECTED_SELECTION_TEXT2 = "Condensed";
279312

@@ -294,10 +327,9 @@ describe("Select general interaction", () => {
294327
assert.ok(selectText.getHTML(false).indexOf(EXPECTED_SELECTION_TEXT2) !== -1, "Select label is correct.");
295328
});
296329

297-
298330
it("Tests aria-label and aria-labelledby", () => {
299-
const select1 = browser.$("#textAreaAriaLabel").shadow$(".ui5-select-root");
300-
const select2 = browser.$("#textAreaAriaLabelledBy").shadow$(".ui5-select-root");
331+
const select1 = browser.$("#textAreaAriaLabel").shadow$(".ui5-select-label-root");
332+
const select2 = browser.$("#textAreaAriaLabelledBy").shadow$(".ui5-select-label-root");
301333
const EXPECTED_ARIA_LABEL1 = "Hello World";
302334
const EXPECTED_ARIA_LABEL2 = "info text";
303335

0 commit comments

Comments
 (0)