Skip to content

Commit cb76cf4

Browse files
authored
feat(ui5-slider, ui5-range-slider): implement a11y spec (#2714)
FIXES: #2513
1 parent 2624c7e commit cb76cf4

10 files changed

+215
-30
lines changed

packages/main/src/RangeSlider.hbs

+48-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,60 @@
11
{{>include "./SliderBase.hbs"}}
22

3+
<span id="{{_id}}-startHandleDesc" class="ui5-hidden-text">{{_ariaHandlesText.startHandleText}}</span>
4+
<span id="{{_id}}-endHandleDesc" class="ui5-hidden-text">{{_ariaHandlesText.endHandleText}}</span>
5+
6+
{{#*inline "progressBar"}}
7+
<div class="ui5-slider-progress-container">
8+
<div class="ui5-slider-progress"
9+
style="{{styles.progress}}"
10+
@focusin="{{_onfocusin}}"
11+
@focusout="{{_onfocusout}}"
12+
role="slider"
13+
tabindex="{{tabIndex}}"
14+
aria-orientation="horizontal"
15+
aria-valuemin="{{min}}"
16+
aria-valuemax="{{max}}"
17+
aria-valuetext="From {{startValue}} to {{endValue}}"
18+
aria-labelledby="{{_id}}-sliderDesc"
19+
aria-disabled="{{_ariaDisabled}}"
20+
></div>
21+
</div>
22+
{{/inline}}
23+
324
{{#*inline "handles"}}
4-
<div class="ui5-slider-handle ui5-slider-handle--start" style="{{styles.startHandle}}" tabindex="{{tabIndex}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}">
25+
<div class="ui5-slider-handle ui5-slider-handle--start"
26+
style="{{styles.startHandle}}"
27+
@focusin="{{_onfocusin}}"
28+
@focusout="{{_onfocusout}}"
29+
role="slider"
30+
tabindex="{{tabIndex}}"
31+
aria-orientation="horizontal"
32+
aria-valuemin="{{min}}"
33+
aria-valuemax="{{max}}"
34+
aria-valuenow="{{startValue}}"
35+
aria-labelledby="{{_id}}-startHandleDesc"
36+
aria-disabled="{{_ariaDisabled}}"
37+
>
538
{{#if showTooltip}}
639
<div class="ui5-slider-tooltip ui5-slider-tooltip--start" style="{{styles.tooltip}}">
740
<span class="ui5-slider-tooltip-value">{{tooltipStartValue}}</span>
841
</div>
942
{{/if}}
1043
</div>
11-
<div class="ui5-slider-handle ui5-slider-handle--end" style="{{styles.endHandle}}" tabindex="{{tabIndex}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}">
44+
45+
<div class="ui5-slider-handle ui5-slider-handle--end"
46+
style="{{styles.endHandle}}"
47+
@focusin="{{_onfocusin}}"
48+
@focusout="{{_onfocusout}}"
49+
role="slider"
50+
tabindex="{{tabIndex}}"
51+
aria-orientation="horizontal"
52+
aria-valuemin="{{min}}"
53+
aria-valuemax="{{max}}"
54+
aria-valuenow="{{endValue}}"
55+
aria-labelledby="{{_id}}-endHandleDesc"
56+
aria-disabled="{{_ariaDisabled}}"
57+
>
1258
{{#if showTooltip}}
1359
<div class="ui5-slider-tooltip ui5-slider-tooltip--end" style="{{styles.tooltip}}">
1460
<span class="ui5-slider-tooltip-value">{{tooltipEndValue}}</span>

packages/main/src/RangeSlider.js

+34-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import {
88
import SliderBase from "./SliderBase.js";
99
import RangeSliderTemplate from "./generated/templates/RangeSliderTemplate.lit.js";
1010

11+
// Texts
12+
import {
13+
RANGE_SLIDER_ARIA_DESCRIPTION,
14+
RANGE_SLIDER_START_HANDLE_DESCRIPTION,
15+
RANGE_SLIDER_END_HANDLE_DESCRIPTION,
16+
} from "./generated/i18n/i18n-defaults.js";
17+
1118
/**
1219
* @public
1320
*/
@@ -119,6 +126,30 @@ class RangeSlider extends SliderBase {
119126
return this.endValue.toFixed(stepPrecision);
120127
}
121128

129+
get _ariaDisabled() {
130+
return this.disabled || undefined;
131+
}
132+
133+
get _ariaLabelledByText() {
134+
return this.i18nBundle.getText(RANGE_SLIDER_ARIA_DESCRIPTION);
135+
}
136+
137+
get _ariaHandlesText() {
138+
const isRTL = this.effectiveDir === "rtl";
139+
const isReversed = this._areValuesReversed();
140+
const ariaHandlesText = {};
141+
142+
if ((isRTL && !isReversed) || (!isRTL && isReversed)) {
143+
ariaHandlesText.startHandleText = this.i18nBundle.getText(RANGE_SLIDER_END_HANDLE_DESCRIPTION);
144+
ariaHandlesText.endHandleText = this.i18nBundle.getText(RANGE_SLIDER_START_HANDLE_DESCRIPTION);
145+
} else {
146+
ariaHandlesText.startHandleText = this.i18nBundle.getText(RANGE_SLIDER_START_HANDLE_DESCRIPTION);
147+
ariaHandlesText.endHandleText = this.i18nBundle.getText(RANGE_SLIDER_END_HANDLE_DESCRIPTION);
148+
}
149+
150+
return ariaHandlesText;
151+
}
152+
122153
/**
123154
* Check if the previously saved state is outdated. That would mean
124155
* either it is the initial rendering or that a property has been changed
@@ -667,19 +698,15 @@ class RangeSlider extends SliderBase {
667698
}
668699

669700
get _startHandle() {
670-
return this.getDomRef().querySelector(".ui5-slider-handle--start");
701+
return this.shadowRoot.querySelector(".ui5-slider-handle--start");
671702
}
672703

673704
get _endHandle() {
674-
return this.getDomRef().querySelector(".ui5-slider-handle--end");
705+
return this.shadowRoot.querySelector(".ui5-slider-handle--end");
675706
}
676707

677708
get _progressBar() {
678-
return this.getDomRef().querySelector(".ui5-slider-progress");
679-
}
680-
681-
get tabIndexProgress() {
682-
return this.tabIndex;
709+
return this.shadowRoot.querySelector(".ui5-slider-progress");
683710
}
684711

685712
get styles() {

packages/main/src/Slider.hbs

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
{{>include "./SliderBase.hbs"}}
22

3+
{{#*inline "progressBar"}}
4+
<div class="ui5-slider-progress-container" aria-hidden="true">
5+
<div class="ui5-slider-progress"
6+
style="{{styles.progress}}"
7+
@focusout="{{_onfocusout}}"
8+
@focusin="{{_onfocusin}}"
9+
tabindex="-1"
10+
></div>
11+
</div>
12+
{{/inline}}
13+
314
{{#*inline "handles"}}
415
<div class="ui5-slider-handle"
516
style="{{styles.handle}}"
6-
tabindex="{{tabIndex}}"
717
@focusout="{{_onfocusout}}"
818
@focusin="{{_onfocusin}}"
19+
role="slider"
20+
tabindex="{{tabIndex}}"
21+
aria-orientation="horizontal"
22+
aria-valuemin="{{min}}"
23+
aria-valuemax="{{max}}"
24+
aria-valuenow="{{value}}"
25+
aria-labelledby="{{_id}}-sliderDesc"
26+
aria-disabled="{{_ariaDisabled}}"
927
data-sap-focus-ref
1028
>
1129
{{#if showTooltip}}

packages/main/src/Slider.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import SliderBase from "./SliderBase.js";
66
// Template
77
import SliderTemplate from "./generated/templates/SliderTemplate.lit.js";
88

9+
// Texts
10+
import {
11+
SLIDER_ARIA_DESCRIPTION,
12+
} from "./generated/i18n/i18n-defaults.js";
13+
914
/**
1015
* @public
1116
*/
@@ -265,8 +270,12 @@ class Slider extends SliderBase {
265270
return this.value.toFixed(stepPrecision);
266271
}
267272

268-
get tabIndexProgress() {
269-
return "-1";
273+
get _ariaDisabled() {
274+
return this.disabled || undefined;
275+
}
276+
277+
get _ariaLabelledByText() {
278+
return this.i18nBundle.getText(SLIDER_ARIA_DESCRIPTION);
270279
}
271280

272281
static async onDefine() {

packages/main/src/SliderBase.hbs

+5-3
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
{{/if}}
2323
{{/if}}
2424

25-
<div class="ui5-slider-progress-container">
26-
<div class="ui5-slider-progress" style="{{styles.progress}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}" tabindex="{{tabIndexProgress}}"></div>
27-
</div>
25+
{{> progressBar}}
26+
2827
{{> handles}}
2928
</div>
3029
</div>
3130

31+
<span id="{{_id}}-sliderDesc" class="ui5-hidden-text">{{_ariaLabelledByText}}</span>
32+
33+
{{#*inline "progressBar"}}{{/inline}}
3234
{{#*inline "handles"}}{{/inline}}

packages/main/src/SliderBase.js

+1-6
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,6 @@ const metadata = {
114114
_hiddenTickmarks: {
115115
type: Boolean,
116116
},
117-
_tabIndex: {
118-
type: String,
119-
defaultValue: "0",
120-
noAttribute: true,
121-
},
122117
},
123118
events: /** @lends sap.ui.webcomponents.main.SliderBase.prototype */ {
124119
/**
@@ -839,7 +834,7 @@ class SliderBase extends UI5Element {
839834
}
840835

841836
get tabIndex() {
842-
return this.disabled ? "-1" : this._tabIndex;
837+
return this.disabled ? "-1" : "0";
843838
}
844839
}
845840

packages/main/src/i18n/messagebundle.properties

+12
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,15 @@ MULTIINPUT_SHOW_MORE_TOKENS={0} More
283283
#XTOL: Tooltip for panel expand title
284284
PANEL_ICON=Expand/Collapse
285285

286+
#XACT: ARIA description for range slider progress
287+
RANGE_SLIDER_ARIA_DESCRIPTION=Range
288+
289+
#XACT: ARIA description for range slider start handle
290+
RANGE_SLIDER_START_HANDLE_DESCRIPTION=Left handle
291+
292+
#XACT: ARIA description for range slider end handle
293+
RANGE_SLIDER_END_HANDLE_DESCRIPTION=Right handle
294+
286295
#XBUT: Rating indicator tooltip text
287296
RATING_INDICATOR_TOOLTIP_TEXT=Rating
288297

@@ -292,6 +301,9 @@ RATING_INDICATOR_TEXT=Rating Indicator
292301
#XACT: ARIA description for the segmented button
293302
SEGMENTEDBUTTON_ARIA_DESCRIPTION=Segmented button
294303

304+
#XACT: ARIA description for slider handle
305+
SLIDER_ARIA_DESCRIPTION=Slider handle
306+
295307
#XACT: ARIA announcement for the switch on
296308
SWITCH_ON=On
297309

packages/main/src/themes/SliderBase.css

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import "./InvisibleTextStyles.css";
2+
13
:host([disabled]) {
24
opacity: var(--_ui5_slider_disabled_opacity);
35
cursor: default;

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

+65-6
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe("Testing Range Slider interactions", () => {
9797
rangeSlider.setProperty("endValue", 30);
9898

9999
rangeSlider.dragAndDrop({ x: -500, y: 1 });
100-
100+
101101
assert.strictEqual(rangeSlider.getProperty("startValue"), 0, "startValue should be 0 as the selected range has reached the start of the Range Slider");
102102
assert.strictEqual(rangeSlider.getProperty("endValue"), 21, "endValue should be 21 and no less, the initially selected range should be preserved");
103103

@@ -203,7 +203,7 @@ describe("Properties synchronization and normalization", () => {
203203
rangeSlider.setProperty("endValue", 300);
204204

205205
assert.strictEqual(rangeSlider.getProperty("endValue"), 200, "value prop should always be lower than the max value");
206-
206+
207207
rangeSlider.setProperty("startValue", 99);
208208

209209
assert.strictEqual(rangeSlider.getProperty("startValue"), 100, "value prop should always be greater than the min value");
@@ -220,7 +220,7 @@ describe("Properties synchronization and normalization", () => {
220220

221221
assert.strictEqual(rangeSlider.getProperty("startValue"), 14, "startValue should not be stepped to the next step (15)");
222222
assert.strictEqual(rangeSlider.getProperty("endValue"), 24, "endValue should not be stepped to the next step (25)");
223-
});
223+
});
224224

225225
it("If the step property or the labelInterval are changed, the tickmarks and labels must be updated also", () => {
226226
const rangeSlider = browser.$("#range-slider-tickmarks-labels");
@@ -234,7 +234,7 @@ describe("Properties synchronization and normalization", () => {
234234
rangeSlider.setProperty("step", 2);
235235

236236
assert.strictEqual(rangeSlider.getProperty("_labels").length, 11, "Labels must be 12 - 1 for every 2 tickmarks (and 4 current value points)");
237-
237+
238238
rangeSlider.setProperty("labelInterval", 4);
239239

240240
assert.strictEqual(rangeSlider.getProperty("_labels").length, 6, "Labels must be 6 - 1 for every 4 tickmarks (and 8 current value points)");
@@ -263,7 +263,66 @@ describe("Testing events", () => {
263263
});
264264

265265

266-
describe("Accessibility: Testing focus", () => {
266+
describe("Accessibility", () => {
267+
it("Aria attributes of the progress bar are set correctly", () => {
268+
const rangeSlider = browser.$("#range-slider-tickmarks");
269+
const rangeSliderProgressBar = rangeSlider.shadow$(".ui5-slider-progress");
270+
const rangeSliderId = rangeSlider.getProperty("_id");
271+
272+
assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-labelledby"),
273+
`${rangeSliderId}-sliderDesc`, "aria-labelledby is set correctly");
274+
assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-valuemin"),
275+
`${rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly");
276+
assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-valuemax"),
277+
`${rangeSlider.getProperty("max")}`, "aria-valuemax is set correctly");
278+
assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-valuetext"),
279+
`From ${rangeSlider.getProperty("startValue")} to ${rangeSlider.getProperty("endValue")}`, "aria-valuetext is set correctly");
280+
});
281+
282+
it("Aria attributes of the start handle are set correctly", () => {
283+
const rangeSlider = browser.$("#range-slider-tickmarks");
284+
const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start");
285+
const rangeSliderId = rangeSlider.getProperty("_id");
286+
287+
assert.strictEqual(startHandle.getAttribute("aria-labelledby"),
288+
`${rangeSliderId}-startHandleDesc`, "aria-labelledby is set correctly");
289+
assert.strictEqual(startHandle.getAttribute("aria-valuemin"),
290+
`${rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly");
291+
assert.strictEqual(startHandle.getAttribute("aria-valuemax"),
292+
`${rangeSlider.getProperty("max")}`, "aria-valuemax is set correctly");
293+
assert.strictEqual(startHandle.getAttribute("aria-valuenow"),
294+
`${rangeSlider.getProperty("startValue")}`, "aria-valuenow is set correctly");
295+
});
296+
297+
it("Aria attributes of the end handle are set correctly", () => {
298+
const rangeSlider = browser.$("#range-slider-tickmarks");
299+
const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end");
300+
const rangeSliderId = rangeSlider.getProperty("_id");
301+
302+
assert.strictEqual(endHandle.getAttribute("aria-labelledby"),
303+
`${rangeSliderId}-endHandleDesc`, "aria-labelledby is set correctly");
304+
assert.strictEqual(endHandle.getAttribute("aria-valuemin"),
305+
`${rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly");
306+
assert.strictEqual(endHandle.getAttribute("aria-valuemax"),
307+
`${rangeSlider.getProperty("max")}`, "aria-valuemax is set correctly");
308+
assert.strictEqual(endHandle.getAttribute("aria-valuenow"),
309+
`${rangeSlider.getProperty("endValue")}`, "aria-valuenow is set correctly");
310+
});
311+
312+
it("Aria-labelledby text is mapped correctly when values are swapped", () => {
313+
const rangeSlider = browser.$("#range-slider-tickmarks");
314+
const rangeSliderId = rangeSlider.getProperty("_id");
315+
const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start");
316+
const rangeSliderStartHandleSpan = rangeSlider.shadow$(`#${rangeSliderId}-startHandleDesc`);
317+
const rangeSliderEndHandleSpan = rangeSlider.shadow$(`#${rangeSliderId}-endHandleDesc`);
318+
319+
rangeSlider.setProperty("endValue", 9);
320+
startHandle.dragAndDrop({ x: 100, y: 1 });
321+
322+
assert.strictEqual(rangeSliderStartHandleSpan.getText(), "Left handle", "Start Handle text is correct after swap");
323+
assert.strictEqual(rangeSliderEndHandleSpan.getText(), "Right handle", "End Handle text is correct after swap");
324+
});
325+
267326
it("Click anywhere in the Range Slider should focus the closest handle", () => {
268327
browser.url("http://localhost:8080/test-resources/pages/RangeSlider.html");
269328

@@ -321,7 +380,7 @@ describe("Accessibility: Testing focus", () => {
321380
assert.strictEqual(rangeSlider.isFocused(), true, "Range Slider component is focused");
322381
assert.strictEqual($(innerFocusedElement).getAttribute("class"), rangeSliderSelection.getAttribute("class"), "Range Slider progress tracker has the shadowDom focus");
323382
});
324-
383+
325384
it("When progress bar has the focus, 'Tab' should move the focus to the first handle", () => {
326385
const rangeSlider = browser.$("#basic-range-slider");
327386
const rangeSliderStartHandle = rangeSlider.shadow$(".ui5-slider-handle--start");

0 commit comments

Comments
 (0)