Skip to content

Commit

Permalink
feat(ui5-slider, ui5-range-slider): implement a11y spec (#2714)
Browse files Browse the repository at this point in the history
FIXES: #2513
  • Loading branch information
ivoplashkov committed Jan 26, 2021
1 parent 2624c7e commit cb76cf4
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 30 deletions.
50 changes: 48 additions & 2 deletions packages/main/src/RangeSlider.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,60 @@
{{>include "./SliderBase.hbs"}}

<span id="{{_id}}-startHandleDesc" class="ui5-hidden-text">{{_ariaHandlesText.startHandleText}}</span>
<span id="{{_id}}-endHandleDesc" class="ui5-hidden-text">{{_ariaHandlesText.endHandleText}}</span>

{{#*inline "progressBar"}}
<div class="ui5-slider-progress-container">
<div class="ui5-slider-progress"
style="{{styles.progress}}"
@focusin="{{_onfocusin}}"
@focusout="{{_onfocusout}}"
role="slider"
tabindex="{{tabIndex}}"
aria-orientation="horizontal"
aria-valuemin="{{min}}"
aria-valuemax="{{max}}"
aria-valuetext="From {{startValue}} to {{endValue}}"
aria-labelledby="{{_id}}-sliderDesc"
aria-disabled="{{_ariaDisabled}}"
></div>
</div>
{{/inline}}

{{#*inline "handles"}}
<div class="ui5-slider-handle ui5-slider-handle--start" style="{{styles.startHandle}}" tabindex="{{tabIndex}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}">
<div class="ui5-slider-handle ui5-slider-handle--start"
style="{{styles.startHandle}}"
@focusin="{{_onfocusin}}"
@focusout="{{_onfocusout}}"
role="slider"
tabindex="{{tabIndex}}"
aria-orientation="horizontal"
aria-valuemin="{{min}}"
aria-valuemax="{{max}}"
aria-valuenow="{{startValue}}"
aria-labelledby="{{_id}}-startHandleDesc"
aria-disabled="{{_ariaDisabled}}"
>
{{#if showTooltip}}
<div class="ui5-slider-tooltip ui5-slider-tooltip--start" style="{{styles.tooltip}}">
<span class="ui5-slider-tooltip-value">{{tooltipStartValue}}</span>
</div>
{{/if}}
</div>
<div class="ui5-slider-handle ui5-slider-handle--end" style="{{styles.endHandle}}" tabindex="{{tabIndex}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}">

<div class="ui5-slider-handle ui5-slider-handle--end"
style="{{styles.endHandle}}"
@focusin="{{_onfocusin}}"
@focusout="{{_onfocusout}}"
role="slider"
tabindex="{{tabIndex}}"
aria-orientation="horizontal"
aria-valuemin="{{min}}"
aria-valuemax="{{max}}"
aria-valuenow="{{endValue}}"
aria-labelledby="{{_id}}-endHandleDesc"
aria-disabled="{{_ariaDisabled}}"
>
{{#if showTooltip}}
<div class="ui5-slider-tooltip ui5-slider-tooltip--end" style="{{styles.tooltip}}">
<span class="ui5-slider-tooltip-value">{{tooltipEndValue}}</span>
Expand Down
41 changes: 34 additions & 7 deletions packages/main/src/RangeSlider.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import {
import SliderBase from "./SliderBase.js";
import RangeSliderTemplate from "./generated/templates/RangeSliderTemplate.lit.js";

// Texts
import {
RANGE_SLIDER_ARIA_DESCRIPTION,
RANGE_SLIDER_START_HANDLE_DESCRIPTION,
RANGE_SLIDER_END_HANDLE_DESCRIPTION,
} from "./generated/i18n/i18n-defaults.js";

/**
* @public
*/
Expand Down Expand Up @@ -119,6 +126,30 @@ class RangeSlider extends SliderBase {
return this.endValue.toFixed(stepPrecision);
}

get _ariaDisabled() {
return this.disabled || undefined;
}

get _ariaLabelledByText() {
return this.i18nBundle.getText(RANGE_SLIDER_ARIA_DESCRIPTION);
}

get _ariaHandlesText() {
const isRTL = this.effectiveDir === "rtl";
const isReversed = this._areValuesReversed();
const ariaHandlesText = {};

if ((isRTL && !isReversed) || (!isRTL && isReversed)) {
ariaHandlesText.startHandleText = this.i18nBundle.getText(RANGE_SLIDER_END_HANDLE_DESCRIPTION);
ariaHandlesText.endHandleText = this.i18nBundle.getText(RANGE_SLIDER_START_HANDLE_DESCRIPTION);
} else {
ariaHandlesText.startHandleText = this.i18nBundle.getText(RANGE_SLIDER_START_HANDLE_DESCRIPTION);
ariaHandlesText.endHandleText = this.i18nBundle.getText(RANGE_SLIDER_END_HANDLE_DESCRIPTION);
}

return ariaHandlesText;
}

/**
* Check if the previously saved state is outdated. That would mean
* either it is the initial rendering or that a property has been changed
Expand Down Expand Up @@ -667,19 +698,15 @@ class RangeSlider extends SliderBase {
}

get _startHandle() {
return this.getDomRef().querySelector(".ui5-slider-handle--start");
return this.shadowRoot.querySelector(".ui5-slider-handle--start");
}

get _endHandle() {
return this.getDomRef().querySelector(".ui5-slider-handle--end");
return this.shadowRoot.querySelector(".ui5-slider-handle--end");
}

get _progressBar() {
return this.getDomRef().querySelector(".ui5-slider-progress");
}

get tabIndexProgress() {
return this.tabIndex;
return this.shadowRoot.querySelector(".ui5-slider-progress");
}

get styles() {
Expand Down
20 changes: 19 additions & 1 deletion packages/main/src/Slider.hbs
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
{{>include "./SliderBase.hbs"}}

{{#*inline "progressBar"}}
<div class="ui5-slider-progress-container" aria-hidden="true">
<div class="ui5-slider-progress"
style="{{styles.progress}}"
@focusout="{{_onfocusout}}"
@focusin="{{_onfocusin}}"
tabindex="-1"
></div>
</div>
{{/inline}}

{{#*inline "handles"}}
<div class="ui5-slider-handle"
style="{{styles.handle}}"
tabindex="{{tabIndex}}"
@focusout="{{_onfocusout}}"
@focusin="{{_onfocusin}}"
role="slider"
tabindex="{{tabIndex}}"
aria-orientation="horizontal"
aria-valuemin="{{min}}"
aria-valuemax="{{max}}"
aria-valuenow="{{value}}"
aria-labelledby="{{_id}}-sliderDesc"
aria-disabled="{{_ariaDisabled}}"
data-sap-focus-ref
>
{{#if showTooltip}}
Expand Down
13 changes: 11 additions & 2 deletions packages/main/src/Slider.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import SliderBase from "./SliderBase.js";
// Template
import SliderTemplate from "./generated/templates/SliderTemplate.lit.js";

// Texts
import {
SLIDER_ARIA_DESCRIPTION,
} from "./generated/i18n/i18n-defaults.js";

/**
* @public
*/
Expand Down Expand Up @@ -265,8 +270,12 @@ class Slider extends SliderBase {
return this.value.toFixed(stepPrecision);
}

get tabIndexProgress() {
return "-1";
get _ariaDisabled() {
return this.disabled || undefined;
}

get _ariaLabelledByText() {
return this.i18nBundle.getText(SLIDER_ARIA_DESCRIPTION);
}

static async onDefine() {
Expand Down
8 changes: 5 additions & 3 deletions packages/main/src/SliderBase.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
{{/if}}
{{/if}}

<div class="ui5-slider-progress-container">
<div class="ui5-slider-progress" style="{{styles.progress}}" @focusout="{{_onfocusout}}" @focusin="{{_onfocusin}}" tabindex="{{tabIndexProgress}}"></div>
</div>
{{> progressBar}}

{{> handles}}
</div>
</div>

<span id="{{_id}}-sliderDesc" class="ui5-hidden-text">{{_ariaLabelledByText}}</span>

{{#*inline "progressBar"}}{{/inline}}
{{#*inline "handles"}}{{/inline}}
7 changes: 1 addition & 6 deletions packages/main/src/SliderBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,6 @@ const metadata = {
_hiddenTickmarks: {
type: Boolean,
},
_tabIndex: {
type: String,
defaultValue: "0",
noAttribute: true,
},
},
events: /** @lends sap.ui.webcomponents.main.SliderBase.prototype */ {
/**
Expand Down Expand Up @@ -839,7 +834,7 @@ class SliderBase extends UI5Element {
}

get tabIndex() {
return this.disabled ? "-1" : this._tabIndex;
return this.disabled ? "-1" : "0";
}
}

Expand Down
12 changes: 12 additions & 0 deletions packages/main/src/i18n/messagebundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,15 @@ MULTIINPUT_SHOW_MORE_TOKENS={0} More
#XTOL: Tooltip for panel expand title
PANEL_ICON=Expand/Collapse

#XACT: ARIA description for range slider progress
RANGE_SLIDER_ARIA_DESCRIPTION=Range

#XACT: ARIA description for range slider start handle
RANGE_SLIDER_START_HANDLE_DESCRIPTION=Left handle

#XACT: ARIA description for range slider end handle
RANGE_SLIDER_END_HANDLE_DESCRIPTION=Right handle

#XBUT: Rating indicator tooltip text
RATING_INDICATOR_TOOLTIP_TEXT=Rating

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

#XACT: ARIA description for slider handle
SLIDER_ARIA_DESCRIPTION=Slider handle

#XACT: ARIA announcement for the switch on
SWITCH_ON=On

Expand Down
2 changes: 2 additions & 0 deletions packages/main/src/themes/SliderBase.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "./InvisibleTextStyles.css";

:host([disabled]) {
opacity: var(--_ui5_slider_disabled_opacity);
cursor: default;
Expand Down
71 changes: 65 additions & 6 deletions packages/main/test/specs/RangeSlider.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe("Testing Range Slider interactions", () => {
rangeSlider.setProperty("endValue", 30);

rangeSlider.dragAndDrop({ x: -500, y: 1 });

assert.strictEqual(rangeSlider.getProperty("startValue"), 0, "startValue should be 0 as the selected range has reached the start of the Range Slider");
assert.strictEqual(rangeSlider.getProperty("endValue"), 21, "endValue should be 21 and no less, the initially selected range should be preserved");

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

assert.strictEqual(rangeSlider.getProperty("endValue"), 200, "value prop should always be lower than the max value");

rangeSlider.setProperty("startValue", 99);

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

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

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

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

rangeSlider.setProperty("labelInterval", 4);

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


describe("Accessibility: Testing focus", () => {
describe("Accessibility", () => {
it("Aria attributes of the progress bar are set correctly", () => {
const rangeSlider = browser.$("#range-slider-tickmarks");
const rangeSliderProgressBar = rangeSlider.shadow$(".ui5-slider-progress");
const rangeSliderId = rangeSlider.getProperty("_id");

assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-labelledby"),
`${rangeSliderId}-sliderDesc`, "aria-labelledby is set correctly");
assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-valuemin"),
`${rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly");
assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-valuemax"),
`${rangeSlider.getProperty("max")}`, "aria-valuemax is set correctly");
assert.strictEqual(rangeSliderProgressBar.getAttribute("aria-valuetext"),
`From ${rangeSlider.getProperty("startValue")} to ${rangeSlider.getProperty("endValue")}`, "aria-valuetext is set correctly");
});

it("Aria attributes of the start handle are set correctly", () => {
const rangeSlider = browser.$("#range-slider-tickmarks");
const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start");
const rangeSliderId = rangeSlider.getProperty("_id");

assert.strictEqual(startHandle.getAttribute("aria-labelledby"),
`${rangeSliderId}-startHandleDesc`, "aria-labelledby is set correctly");
assert.strictEqual(startHandle.getAttribute("aria-valuemin"),
`${rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly");
assert.strictEqual(startHandle.getAttribute("aria-valuemax"),
`${rangeSlider.getProperty("max")}`, "aria-valuemax is set correctly");
assert.strictEqual(startHandle.getAttribute("aria-valuenow"),
`${rangeSlider.getProperty("startValue")}`, "aria-valuenow is set correctly");
});

it("Aria attributes of the end handle are set correctly", () => {
const rangeSlider = browser.$("#range-slider-tickmarks");
const endHandle = rangeSlider.shadow$(".ui5-slider-handle--end");
const rangeSliderId = rangeSlider.getProperty("_id");

assert.strictEqual(endHandle.getAttribute("aria-labelledby"),
`${rangeSliderId}-endHandleDesc`, "aria-labelledby is set correctly");
assert.strictEqual(endHandle.getAttribute("aria-valuemin"),
`${rangeSlider.getProperty("min")}`, "aria-valuemin is set correctly");
assert.strictEqual(endHandle.getAttribute("aria-valuemax"),
`${rangeSlider.getProperty("max")}`, "aria-valuemax is set correctly");
assert.strictEqual(endHandle.getAttribute("aria-valuenow"),
`${rangeSlider.getProperty("endValue")}`, "aria-valuenow is set correctly");
});

it("Aria-labelledby text is mapped correctly when values are swapped", () => {
const rangeSlider = browser.$("#range-slider-tickmarks");
const rangeSliderId = rangeSlider.getProperty("_id");
const startHandle = rangeSlider.shadow$(".ui5-slider-handle--start");
const rangeSliderStartHandleSpan = rangeSlider.shadow$(`#${rangeSliderId}-startHandleDesc`);
const rangeSliderEndHandleSpan = rangeSlider.shadow$(`#${rangeSliderId}-endHandleDesc`);

rangeSlider.setProperty("endValue", 9);
startHandle.dragAndDrop({ x: 100, y: 1 });

assert.strictEqual(rangeSliderStartHandleSpan.getText(), "Left handle", "Start Handle text is correct after swap");
assert.strictEqual(rangeSliderEndHandleSpan.getText(), "Right handle", "End Handle text is correct after swap");
});

it("Click anywhere in the Range Slider should focus the closest handle", () => {
browser.url("http://localhost:8080/test-resources/pages/RangeSlider.html");

Expand Down Expand Up @@ -321,7 +380,7 @@ describe("Accessibility: Testing focus", () => {
assert.strictEqual(rangeSlider.isFocused(), true, "Range Slider component is focused");
assert.strictEqual($(innerFocusedElement).getAttribute("class"), rangeSliderSelection.getAttribute("class"), "Range Slider progress tracker has the shadowDom focus");
});

it("When progress bar has the focus, 'Tab' should move the focus to the first handle", () => {
const rangeSlider = browser.$("#basic-range-slider");
const rangeSliderStartHandle = rangeSlider.shadow$(".ui5-slider-handle--start");
Expand Down
Loading

0 comments on commit cb76cf4

Please sign in to comment.