Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-slider, ui5-range-slider): Implement accessibility specifications #2714

Merged
merged 10 commits into from
Jan 26, 2021
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="0"
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="0"
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="0"
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");
}

ivoplashkov marked this conversation as resolved.
Show resolved Hide resolved
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="0"
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}}
9 changes: 0 additions & 9 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 @@ -837,10 +832,6 @@ class SliderBase extends UI5Element {
get _effectiveMax() {
return Math.max(this.min, this.max);
}

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

export default SliderBase;
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