From 937cfab628be70371ceff85f2c4002710e377059 Mon Sep 17 00:00:00 2001
From: Jon Gunderson
Date: Tue, 24 Aug 2021 09:16:10 -0500
Subject: [PATCH] Add Example: Media Seek Slider (pull #1863)
Adds a horizontal slider that illustrates:
* Appending max value to aria-valuetext on focus, but not on change in value.
* Using SVG graphics elements
* Increased size of clickable area on rail to change value with pointing device
* High contrast support
* Latest APG coding practices, including:
* Use of event.key.
* Use of pointer events.
* CSS property forced-color-adjust to auto on the SVG elements.
* Using stroke-opacity and fill-opacity instead of transparent values for setting stroke and fill colors for SVG rect to support focus ring visibility in high contrast modes.
Co-authored-by: Matt King
---
aria-practices.html | 3 +-
examples/index.html | 8 +
examples/slider/css/slider-seek.css | 81 ++++
examples/slider/js/slider-seek.js | 360 +++++++++++++++++
examples/slider/slider-color-viewer.html | 3 +-
examples/slider/slider-rating.html | 3 +-
examples/slider/slider-seek.html | 335 ++++++++++++++++
examples/slider/slider-temperature.html | 1 +
test/tests/slider_slider-seek.js | 476 +++++++++++++++++++++++
9 files changed, 1267 insertions(+), 3 deletions(-)
create mode 100644 examples/slider/css/slider-seek.css
create mode 100644 examples/slider/js/slider-seek.js
create mode 100644 examples/slider/slider-seek.html
create mode 100644 test/tests/slider_slider-seek.js
diff --git a/aria-practices.html b/aria-practices.html
index 6d9d8eeede..10a16ba1f6 100644
--- a/aria-practices.html
+++ b/aria-practices.html
@@ -2363,8 +2363,9 @@
Slider
Examples
Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
-
Temperature Selector Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
+
Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum value of a rating input for a five star rating scale.
+
Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
diff --git a/examples/slider/css/slider-seek.css b/examples/slider/css/slider-seek.css
new file mode 100644
index 0000000000..b746a74303
--- /dev/null
+++ b/examples/slider/css/slider-seek.css
@@ -0,0 +1,81 @@
+/* CSS Document */
+
+.slider-seek .label {
+ font-weight: bold;
+ font-size: 125%;
+}
+
+.slider-seek svg {
+ forced-color-adjust: auto;
+}
+
+.slider-seek text {
+ font-weight: bold;
+ fill: currentColor;
+ font-family: sans-serif;
+}
+
+.slider-seek {
+ margin-top: 1em;
+ padding: 6px;
+ color: black;
+}
+
+.slider-slider .value {
+ position: relative;
+ top: 20px;
+ height: 1.5em;
+ font-size: 80%;
+}
+
+.slider-seek .temp-value {
+ padding-left: 24px;
+ font-size: 200%;
+}
+
+.slider-seek .rail {
+ stroke: currentColor;
+ stroke-width: 2px;
+ fill: currentColor;
+ fill-opacity: 25%;
+}
+
+.slider-seek .thumb {
+ stroke-width: 0;
+ fill: currentColor;
+}
+
+.slider-seek .focus-ring {
+ stroke: currentColor;
+ stroke-opacity: 0;
+ fill-opacity: 0;
+ stroke-width: 3px;
+ display: none;
+}
+
+.slider-seek .slider-group {
+ touch-action: pan-y;
+}
+
+.slider-seek .slider-group .value {
+ display: none;
+}
+
+/* Focus and hover styling */
+
+.slider-seek.focus [role="slider"] {
+ color: #005a9c;
+}
+
+.slider-seek [role="slider"]:focus {
+ outline: none;
+}
+
+.slider-seek [role="slider"]:focus .focus-ring {
+ display: block;
+ stroke-opacity: 1;
+}
+
+.slider-seek [role="slider"]:focus .value {
+ display: block;
+}
diff --git a/examples/slider/js/slider-seek.js b/examples/slider/js/slider-seek.js
new file mode 100644
index 0000000000..725a54917a
--- /dev/null
+++ b/examples/slider/js/slider-seek.js
@@ -0,0 +1,360 @@
+/*
+ * This content is licensed according to the W3C Software License at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
+ *
+ * File: slider-valuetext.js
+ *
+ * Desc: Slider widgets using aria-valuetext that implements ARIA Authoring Practices
+ */
+
+'use strict';
+
+/*
+ * Desc: Slider widget that implements ARIA Authoring Practices
+ */
+
+class SliderSeek {
+ constructor(domNode) {
+ this.domNode = domNode;
+
+ this.isMoving = false;
+
+ this.svgNode = domNode.querySelector('svg');
+ this.svgPoint = this.svgNode.createSVGPoint();
+
+ this.railNode = domNode.querySelector('.rail');
+ this.sliderNode = domNode.querySelector('[role=slider]');
+ this.sliderValueNode = this.sliderNode.querySelector('.value');
+ this.sliderFocusNode = this.sliderNode.querySelector('.focus-ring');
+ this.sliderThumbNode = this.sliderNode.querySelector('.thumb');
+
+ this.valueLabelNodes = domNode.querySelectorAll('.value-label');
+
+ // Dimensions of the slider focus ring, thumb and rail
+
+ this.railHeight = parseInt(this.railNode.getAttribute('height'));
+ this.railWidth = parseInt(this.railNode.getAttribute('width'));
+ this.railY = parseInt(this.railNode.getAttribute('y'));
+ this.railX = parseInt(this.railNode.getAttribute('x'));
+
+ this.thumbWidth = parseInt(this.sliderThumbNode.getAttribute('width'));
+ this.thumbHeight = parseInt(this.sliderThumbNode.getAttribute('height'));
+
+ this.focusHeight = parseInt(this.sliderFocusNode.getAttribute('height'));
+ this.focusWidth = parseInt(this.sliderFocusNode.getAttribute('width'));
+
+ this.thumbY = this.railY + this.railHeight / 2 - this.thumbHeight / 2;
+ this.sliderThumbNode.setAttribute('y', this.thumbY);
+
+ this.focusY = this.railY + this.railHeight / 2 - this.focusHeight / 2;
+ this.sliderFocusNode.setAttribute('y', this.focusY);
+
+ this.railNode.setAttribute('y', this.railY);
+ this.railNode.setAttribute('x', this.railX);
+ this.railNode.setAttribute('height', this.railHeight);
+ this.railNode.setAttribute('width', this.railWidth);
+
+ // define possible slider positions
+
+ this.svgNode.addEventListener('click', this.onRailClick.bind(this));
+ this.sliderNode.addEventListener(
+ 'keydown',
+ this.onSliderKeydown.bind(this)
+ );
+
+ this.sliderNode.addEventListener(
+ 'pointerdown',
+ this.onSliderPointerDown.bind(this)
+ );
+
+ // bind a pointermove event handler to move pointer
+ this.svgNode.addEventListener('pointermove', this.onPointerMove.bind(this));
+
+ // bind a pointerup event handler to stop tracking pointer movements
+ document.addEventListener('pointerup', this.onPointerUp.bind(this));
+
+ this.sliderNode.addEventListener('focus', this.onSliderFocus.bind(this));
+ this.sliderNode.addEventListener('blur', this.onSliderBlur.bind(this));
+
+ let deltaPosition = this.railWidth / (this.valueLabelNodes.length - 1);
+
+ let position = this.railX;
+
+ this.positions = [];
+ this.textValues = [];
+
+ let maxTextWidth = this.getWidthFromLabelText();
+ let textHeight = this.getHeightFromLabelText();
+
+ for (let i = 0; i < this.valueLabelNodes.length; i++) {
+ let valueLabelNode = this.valueLabelNodes[i];
+
+ let textNode = valueLabelNode.querySelector('text');
+
+ let w = maxTextWidth + 2;
+ let x = position - w / 2;
+ let y = this.thumbY + this.thumbHeight;
+
+ x = x + (maxTextWidth - textNode.getBoundingClientRect().width) / 2;
+ y = y + textHeight;
+
+ textNode.setAttribute('x', x);
+ textNode.setAttribute('y', y);
+
+ this.textValues.push(valueLabelNode.getAttribute('data-value'));
+
+ this.positions.push(position);
+ position += deltaPosition;
+ }
+
+ // temporarily show slider value to allow width calc onload
+ this.sliderValueNode.setAttribute('style', 'display: block');
+ this.moveSliderTo(this.getValue());
+ this.sliderValueNode.removeAttribute('style');
+
+ // Include total time in aria-valuetext when loaded
+ this.sliderNode.setAttribute(
+ 'aria-valuetext',
+ this.getValueTextMinutesSeconds(this.getValue(), true)
+ );
+ }
+
+ getWidthFromLabelText() {
+ let width = 0;
+ for (let i = 0; i < this.valueLabelNodes.length; i++) {
+ let textNode = this.valueLabelNodes[i].querySelector('text');
+ if (textNode) {
+ width = Math.max(width, textNode.getBoundingClientRect().width);
+ }
+ }
+ return width;
+ }
+
+ getHeightFromLabelText() {
+ let height = 0;
+ let textNode = this.valueLabelNodes[0].querySelector('text');
+ if (textNode) {
+ height = textNode.getBoundingClientRect().height;
+ }
+ return height;
+ }
+
+ // Get point in global SVG space
+ getSVGPoint(event) {
+ this.svgPoint.x = event.clientX;
+ this.svgPoint.y = event.clientY;
+ return this.svgPoint.matrixTransform(this.svgNode.getScreenCTM().inverse());
+ }
+
+ getValue() {
+ return parseInt(this.sliderNode.getAttribute('aria-valuenow'));
+ }
+
+ getValueMin() {
+ return parseInt(this.sliderNode.getAttribute('aria-valuemin'));
+ }
+
+ getValueMax() {
+ return parseInt(this.sliderNode.getAttribute('aria-valuemax'));
+ }
+
+ isInRange(value) {
+ let valueMin = this.getValueMin();
+ let valueMax = this.getValueMax();
+ return value <= valueMax && value >= valueMin;
+ }
+
+ getValueMinutesSeconds(value) {
+ let minutes = parseInt(value / 60);
+ let seconds = value % 60;
+
+ if (seconds < 10) {
+ seconds = '0' + seconds;
+ }
+ return minutes + ':' + seconds;
+ }
+
+ getValueTextMinutesSeconds(value, flag) {
+ if (typeof flag !== 'boolean') {
+ flag = false;
+ }
+
+ let minutes = parseInt(value / 60);
+ let seconds = value % 60;
+
+ let valuetext = '';
+ let minutesLabel = 'Minutes';
+ let secondsLabel = 'Seconds';
+
+ if (minutes === 1) {
+ minutesLabel = 'Minute';
+ }
+
+ if (minutes > 0) {
+ valuetext += minutes + ' ' + minutesLabel;
+ }
+
+ if (seconds === 1) {
+ secondsLabel = 'Second';
+ }
+
+ if (seconds > 0) {
+ if (minutes > 0) {
+ valuetext += ' ';
+ }
+ valuetext += seconds + ' ' + secondsLabel;
+ }
+
+ if (minutes === 0 && seconds === 0) {
+ valuetext += '0 ' + secondsLabel;
+ }
+
+ if (flag) {
+ let maxValue = parseInt(this.sliderNode.getAttribute('aria-valuemax'));
+ valuetext += ' of ' + this.getValueTextMinutesSeconds(maxValue);
+ }
+
+ return valuetext;
+ }
+
+ moveSliderTo(value) {
+ let valueMax, valueMin, pos, width;
+
+ valueMin = this.getValueMin();
+ valueMax = this.getValueMax();
+
+ value = Math.min(Math.max(value, valueMin), valueMax);
+
+ this.sliderNode.setAttribute('aria-valuenow', value);
+
+ this.sliderValueNode.textContent = this.getValueMinutesSeconds(value);
+
+ width = this.sliderValueNode.getBoundingClientRect().width;
+
+ this.sliderNode.setAttribute(
+ 'aria-valuetext',
+ this.getValueTextMinutesSeconds(value)
+ );
+
+ pos =
+ this.railX +
+ Math.round(((value - valueMin) * this.railWidth) / (valueMax - valueMin));
+
+ // move the SVG focus ring and thumb elements
+ this.sliderFocusNode.setAttribute('x', pos - this.focusWidth / 2);
+ this.sliderThumbNode.setAttribute('x', pos - this.thumbWidth / 2);
+ this.sliderValueNode.setAttribute('x', pos - width / 2);
+ }
+
+ onSliderKeydown(event) {
+ var flag = false;
+ var value = this.getValue();
+ var valueMin = this.getValueMin();
+ var valueMax = this.getValueMax();
+
+ switch (event.key) {
+ case 'ArrowLeft':
+ case 'ArrowDown':
+ this.moveSliderTo(value - 1);
+ flag = true;
+ break;
+
+ case 'ArrowRight':
+ case 'ArrowUp':
+ this.moveSliderTo(value + 1);
+ flag = true;
+ break;
+
+ case 'PageDown':
+ this.moveSliderTo(value - 15);
+ flag = true;
+ break;
+
+ case 'PageUp':
+ this.moveSliderTo(value + 15);
+ flag = true;
+ break;
+
+ case 'Home':
+ this.moveSliderTo(valueMin);
+ flag = true;
+ break;
+
+ case 'End':
+ this.moveSliderTo(valueMax);
+ flag = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (flag) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ onSliderFocus() {
+ this.domNode.classList.add('focus');
+ }
+
+ onSliderBlur() {
+ this.domNode.classList.remove('focus');
+ // Include total time in aria-valuetext
+ this.sliderNode.setAttribute(
+ 'aria-valuetext',
+ this.getValueTextMinutesSeconds(this.getValue(), true)
+ );
+ }
+
+ onRailClick(event) {
+ var x = this.getSVGPoint(event).x;
+ var min = this.getValueMin();
+ var max = this.getValueMax();
+ var diffX = x - this.railX;
+ var value = Math.round((diffX * (max - min)) / this.railWidth);
+ this.moveSliderTo(value);
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Set focus to the clicked handle
+ this.sliderNode.focus();
+ }
+
+ onSliderPointerDown(event) {
+ this.isMoving = true;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Set focus to the clicked handle
+ this.sliderNode.focus();
+ }
+
+ onPointerMove(event) {
+ if (this.isMoving) {
+ var x = this.getSVGPoint(event).x;
+ var min = this.getValueMin();
+ var max = this.getValueMax();
+ var diffX = x - this.railX;
+ var value = Math.round((diffX * (max - min)) / this.railWidth);
+ this.moveSliderTo(value);
+
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ onPointerUp() {
+ this.isMoving = false;
+ }
+}
+
+window.addEventListener('load', function () {
+ let sliders = document.querySelectorAll('.slider-seek');
+ for (let i = 0; i < sliders.length; i++) {
+ new SliderSeek(sliders[i]);
+ }
+});
diff --git a/examples/slider/slider-color-viewer.html b/examples/slider/slider-color-viewer.html
index b278585962..abadc43a20 100644
--- a/examples/slider/slider-color-viewer.html
+++ b/examples/slider/slider-color-viewer.html
@@ -43,8 +43,9 @@
Color Viewer Slider Example
Similar examples include:
-
Temperature Selector Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
+
Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum value of a rating input for a five star rating scale.
+
Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
Horizontal Multi-Thumb Slider Example: Demonstrates using sliders with two thumbs to provide inputs for numeric ranges, such as for searching in a price range.
Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
-
Temperature Selector Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
+
Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
+
Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
Horizontal Multi-Thumb Slider Example: Demonstrates using sliders with two thumbs to provide inputs for numeric ranges, such as for searching in a price range.
+ WARNING! Some users of touch-based assistive technologies may experience difficulty utilizing widgets that implement this slider pattern because the gestures their assistive technology provides for operating sliders may not yet generate the necessary output.
+ To change the slider value, touch-based assistive technologies need to respond to user gestures for increasing and decreasing the value by synthesizing key events.
+ This is a new convention that may not be fully implemented by some assistive technologies.
+ Authors should fully test slider widgets using assistive technologies on devices where touch is a primary input mechanism before considering incorporation into production systems.
+
+
+
+ The following example of the
+ slider design pattern
+ illustrates a seek control that could be used to move the current play position in an audio or video media player.
+ The example demonstrates how to use aria-valuetext to provide assistive technology users with meaningful names for numeric values.
+ In this case, the value of the control is the position in seconds.
+ For example, if the play position were 4 minutes and 3 seconds from the start, the slider value is 243.
+ If aria-valuetext were not used, assistive technology users would be told that the position is 243, which is significantly more difficult to comprehend than 4 minutes, 3 seconds.
+ So, the code converts the slider value to a string that communicates the position in minutes and seconds and provides that value via aria-valuetext.
+
+
Similar examples include:
+
+
Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
+
Vertical Temperature Slider Example: Demonstrates using aria-orientation to specify vertical orientation and aria-valuetext to communicate unit of measure for a temperature input.
+
Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum value of a rating input for a five star rating scale.
+
Horizontal Multi-Thumb Slider Example: Demonstrates using sliders with two thumbs to provide inputs for numeric ranges, such as for searching in a price range.
+
+
+
+
+
Example
+
+
+
+
+
Seek
+
+
+
+
+
+
+
+
Accessibility Features
+
+
+ To ensure assistive technology users correctly perceive the maximum slider value, this example uses the aria-valuetext property to communicate both the current and maximum values.
+ However, since repeating the maximum value every time the slider value changes is potentially distracting, the maximum value is included in aria-valuetext only when the slider is initialized and when the thumb loses keyboard focus.
+
+
The display of the slider's current value remains adjacent to the thumb as the thumb is moved, so people with a small field of view (e.g., due to magnification) can easily see the value while focusing on the thumb as they move it.
+
+ To ensure the borders of the slider rail, thumb and focus ring have sufficient contrast with the background when high contrast settings invert colors, the color of the borders are synchronized with the color of the text content.
+ For example, the color of the focus ring border is set to match the foreground color of high contrast mode text by specifying the CSS currentColor value for the stroke property of the inline SVG rect element used to draw the focus ring border.
+ To make the background of the rect match the high contrast background color, the fill-opacity attribute of the rect is set to zero.
+ If specific colors were instead used to specify the stroke and fill properties, those colors would remain the same in high contrast mode, which could lead to insufficient contrast between the rail and the thumb or even make them invisible if their color matched the high contrast mode background.
+ Note: The SVG element needs to have the CSS forced-color-adjust property set to auto for the currentColor value to be updated in high contrast mode.
+ Some browsers do not use auto for the default value.
+
+
+
+
+
+
Keyboard Support
+
+
+
+
Key
+
Function
+
+
+
+
+
+ Right Arrow
+
+
Increases slider value one step.
+
+
+
+ Up Arrow
+
+
Increases slider value one step.
+
+
+
+ Left Arrow
+
+
Decreases slider value one step.
+
+
+
+ Down Arrow
+
+
Decreases slider value one step.
+
+
+
+ Page Up
+
+
Increases slider value 15 steps.
+
+
+
+ Page Down
+
+
Decreases slider value 15 steps.
+
+
+
+ Home
+
+
Sets slider to its minimum value.
+
+
+
+ End
+
+
Sets slider to its maximum value.
+
+
+
+
+
+
+
Role, Property, State, and Tabindex Attributes
+
+
+
+
+
Role
+
Attribute
+
Element
+
Usage
+
+
+
+
+
+ none
+
+
+
+ svg
+
+
+ The use of the none role on the SVG element ensures assistive technologies do not interpret the SVG element as an image or some other role.
+
+
+
+
+ slider
+
+
+
+ g
+
+
+
+
Identifies the element as a slider.
+
Set on the g element that represents as the movable thumb because it is the operable element that represents the slider value.
+
+
+
+
+
+
+ tabindex="0"
+
+
+ g
+
+
Includes the slider thumb in the page tab sequence.
+
+
+
+
+ aria-valuemax="NUMBER"
+
+
+ g
+
+
Specifies a numeric value that is the maximum value the slider can have.
+
+
+
+
+ aria-valuemin="NUMBER"
+
+
+ g
+
+
Specifies a numeric value that is the minimum value the slider can have.
+
+
+
+
+ aria-valuenow="NUMBER"
+
+
+ g
+
+
A numeric value that is the current value of the slider.
+
+
+
+
+ aria-valuetext="STRING"
+
+
+ g
+
+
+
+
A string value that provides a user-friendly name for the current value of the slider. In this example, it is the value converted to minutes and seconds.
+
Additionally includes the maximum value in minutes when the slider is initialized and when the thumb receives keyboard focus.
+
Note: To prevent unnecessary screen reader verbosity, the maximum value is not included when the value changes.
+
+
+
+
+
+
+ aria-labelledby="IDREF"
+
+
+ g
+
+
Refers to the element containing the name (e.g. label) of the slider.
+
+
+
+
+ aria-hidden="true"
+
+
+ rect
+
+
Removes the SVG rect element from the accessibility tree to prevent assistive technologies from presenting it as an image separate from the slider.
Color Viewer Slider Example: Basic horizontal sliders that illustrate setting numeric values for a color picker.
Rating Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum value of a rating input for a five star rating scale.
+
Media Seek Slider Example: Horizontal slider that demonstrates using aria-valuetext to communicate current and maximum values of time in media to make the values easy to understand for assistive technology users by converting the total number of seconds to minutes and seconds.
Horizontal Multi-Thumb Slider Example: Demonstrates using sliders with two thumbs to provide inputs for numeric ranges, such as for searching in a price range.
diff --git a/test/tests/slider_slider-seek.js b/test/tests/slider_slider-seek.js
new file mode 100644
index 0000000000..8195be3fc6
--- /dev/null
+++ b/test/tests/slider_slider-seek.js
@@ -0,0 +1,476 @@
+const { ariaTest } = require('..');
+const { By, Key } = require('selenium-webdriver');
+const assertAttributeValues = require('../util/assertAttributeValues');
+const assertAriaLabelledby = require('../util/assertAriaLabelledby');
+const assertAriaRoles = require('../util/assertAriaRoles');
+
+const exampleFile = 'slider/slider-seek.html';
+
+const ex = {
+ sliderSelector: '#ex1 [role="slider"]',
+ railRects: '#ex1 rect.rail',
+ seekSelector: '#id-seek',
+ seekValueSelector: '#id-seek-slider .value',
+ seekMax: '300',
+ seekMin: '0',
+ seekDefault: '90',
+ seekDefaultValue: '1 Minute 30 Seconds of 5 Minutes',
+ seekInc: 1,
+ seekPageInc: 15,
+};
+
+const sendAllSlidersToEnd = async function (t) {
+ const sliders = await t.context.queryElements(t, ex.sliderSelector);
+
+ for (let slider of sliders) {
+ await slider.sendKeys(Key.END);
+ }
+};
+
+const getSeekValueAndText = function (v, change) {
+ let minutesLabel = 'Minutes';
+ let secondsLabel = 'Seconds';
+ let valuetext = '';
+
+ v = parseInt(v) + change;
+ if (v > parseInt(ex.seekMax)) {
+ v = parseInt(ex.seekMax);
+ }
+ if (v < parseInt(ex.seekMin)) {
+ v = parseInt(ex.seekMin);
+ }
+ const value = v.toString();
+
+ let minutes = parseInt(v / 60);
+ let seconds = v % 60;
+
+ if (minutes === 1) {
+ minutesLabel = 'Minute';
+ }
+
+ if (minutes > 0) {
+ valuetext += minutes + ' ' + minutesLabel;
+ }
+
+ if (seconds === 1) {
+ secondsLabel = 'Second';
+ }
+
+ if (seconds > 0) {
+ if (minutes > 0) {
+ valuetext += ' ';
+ }
+ valuetext += seconds + ' ' + secondsLabel;
+ }
+
+ if (minutes === 0 && seconds === 0) {
+ valuetext += '0 ' + secondsLabel;
+ }
+
+ return [value, valuetext];
+};
+
+const getValueAndText = async function (t, selector) {
+ const slider = await t.context.session.findElement(By.css(selector));
+ const value = await slider.getAttribute('aria-valuenow');
+ const text = await slider.getAttribute('aria-valuetext');
+ return [value, text];
+};
+
+// Attributes
+
+ariaTest('role="none" on SVG element', exampleFile, 'svg-none', async (t) => {
+ await assertAriaRoles(t, 'ex1', 'none', '1', 'svg');
+});
+
+ariaTest(
+ 'SVG rects used for the rail have aria-hidden',
+ exampleFile,
+ 'aria-hidden-rect',
+ async (t) => {
+ await assertAttributeValues(t, ex.railRects, 'aria-hidden', 'true');
+ }
+);
+
+ariaTest(
+ 'role="slider" on SVG g element',
+ exampleFile,
+ 'slider-role',
+ async (t) => {
+ await assertAriaRoles(t, 'ex1', 'slider', '1', 'g');
+ }
+);
+
+ariaTest(
+ '"tabindex" set to "0" on sliders',
+ exampleFile,
+ 'slider-tabindex',
+ async (t) => {
+ await assertAttributeValues(t, ex.sliderSelector, 'tabindex', '0');
+ }
+);
+
+ariaTest(
+ '"aria-valuemax" set on sliders',
+ exampleFile,
+ 'aria-valuemax',
+ async (t) => {
+ await assertAttributeValues(
+ t,
+ ex.seekSelector,
+ 'aria-valuemax',
+ ex.seekMax
+ );
+ }
+);
+
+ariaTest(
+ '"aria-valuemin" set on sliders',
+ exampleFile,
+ 'aria-valuemin',
+ async (t) => {
+ await assertAttributeValues(
+ t,
+ ex.seekSelector,
+ 'aria-valuemin',
+ ex.seekMin
+ );
+ }
+);
+
+ariaTest(
+ '"aria-valuenow" reflects slider value',
+ exampleFile,
+ 'aria-valuenow',
+ async (t) => {
+ await assertAttributeValues(
+ t,
+ ex.seekSelector,
+ 'aria-valuenow',
+ ex.seekDefault
+ );
+ }
+);
+
+ariaTest(
+ '"aria-valuetext" reflects slider value',
+ exampleFile,
+ 'aria-valuetext',
+ async (t) => {
+ await assertAttributeValues(
+ t,
+ ex.seekSelector,
+ 'aria-valuetext',
+ ex.seekDefaultValue
+ );
+ }
+);
+
+ariaTest(
+ '"aria-labelledby" set on sliders',
+ exampleFile,
+ 'aria-labelledby',
+ async (t) => {
+ await assertAriaLabelledby(t, ex.sliderSelector);
+ }
+);
+
+// Keys
+
+ariaTest(
+ 'Right arrow increases slider value by 1',
+ exampleFile,
+ 'key-right-arrow',
+ async (t) => {
+ // Send 1 key to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.ARROW_RIGHT);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekDefault, 1);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 1 arrow right key to the seek slider, aria-valuenow should be " + value + " and aria-value-text should be: ' +
+ text
+ );
+
+ // Send more than 5 keys to seek slider
+ for (let i = 0; i < 5; i++) {
+ await seekSlider.sendKeys(Key.ARROW_RIGHT);
+ }
+
+ [value, text] = getSeekValueAndText(value, 5);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 5 arrow right key to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);
+
+ariaTest(
+ 'up arrow increases slider value by 1',
+ exampleFile,
+ 'key-up-arrow',
+ async (t) => {
+ // Send 1 key to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.ARROW_UP);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekDefault, ex.seekInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 1 arrow up key to the seek slider, aria-valuenow should be ' +
+ value +
+ ' and aria-value-text should be: ' +
+ text
+ );
+
+ // Send more than 5 keys to seek slider
+ for (let i = 0; i < 5; i++) {
+ await seekSlider.sendKeys(Key.ARROW_UP);
+ }
+
+ [value, text] = getSeekValueAndText(value, 5 * ex.seekInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 6 arrow up key to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);
+
+ariaTest(
+ 'page up increases slider value by big step',
+ exampleFile,
+ 'key-page-up',
+ async (t) => {
+ // Send 1 Page Up key to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.PAGE_UP);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekDefault, ex.seekPageInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 1 page up key to the seek slider, aria-valuenow should be ' +
+ value +
+ ' and aria-value-text should be: ' +
+ text
+ );
+
+ // Send 5 more page up keys to seek slider
+ for (let i = 0; i < 5; i++) {
+ await seekSlider.sendKeys(Key.PAGE_UP);
+ }
+
+ [value, text] = getSeekValueAndText(value, 5 * ex.seekPageInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 6 arrow page up keys to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);
+
+ariaTest(
+ 'key end set slider at max value',
+ exampleFile,
+ 'key-end',
+ async (t) => {
+ // Send key end to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.END);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekMax, 0);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending key end to the heat slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);
+
+ariaTest(
+ 'left arrow decreases slider value by 1',
+ exampleFile,
+ 'key-left-arrow',
+ async (t) => {
+ await sendAllSlidersToEnd(t);
+
+ // Send 1 key to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.ARROW_LEFT);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekMax, -1 * ex.seekInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 1 arrow left key to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+
+ // Send more than 5 keys to seek slider
+ for (let i = 0; i < 5; i++) {
+ await seekSlider.sendKeys(Key.ARROW_LEFT);
+ }
+
+ [value, text] = getSeekValueAndText(value, -5 * ex.seekInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 6 arrow left key to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);
+
+ariaTest(
+ 'down arrow decreases slider value by 1',
+ exampleFile,
+ 'key-down-arrow',
+ async (t) => {
+ await sendAllSlidersToEnd(t);
+
+ // Send 1 key to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.ARROW_DOWN);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekMax, -1 * ex.seekInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 1 down arrow key to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+
+ // Send more than 5 keys to seek slider
+ for (let i = 0; i < 5; i++) {
+ await seekSlider.sendKeys(Key.ARROW_DOWN);
+ }
+
+ [value, text] = getSeekValueAndText(value, -5 * ex.seekInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 6 page arrow key to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);
+
+ariaTest(
+ 'page down decreases slider value by big step',
+ exampleFile,
+ 'key-page-down',
+ async (t) => {
+ // Send 1 Page Down key to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.PAGE_DOWN);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekDefault, -1 * ex.seekPageInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 1 page down key to the seek slider, aria-valuenow should be ' +
+ value +
+ ' and aria-value-text should be: ' +
+ text
+ );
+
+ // Send 5 more page up keys to seek slider
+ for (let i = 0; i < 5; i++) {
+ await seekSlider.sendKeys(Key.PAGE_DOWN);
+ }
+
+ [value, text] = getSeekValueAndText(value, -5 * ex.seekPageInc);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending 6 arrow page down keys to the seek slider, aria-valuenow should be "' +
+ value +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);
+
+ariaTest(
+ 'home set slider value to minimum',
+ exampleFile,
+ 'key-home',
+ async (t) => {
+ // Send key home to seek slider
+ const seekSlider = await t.context.session.findElement(
+ By.css(ex.seekSelector)
+ );
+ await seekSlider.sendKeys(Key.HOME);
+
+ let value, text;
+ [value, text] = getSeekValueAndText(ex.seekMin, 0);
+
+ t.deepEqual(
+ await getValueAndText(t, ex.seekSelector),
+ [value, text],
+ 'After sending key home to the heat slider, aria-valuenow should be "' +
+ ex.seekMin +
+ '" and aria-value-text should be: ' +
+ text
+ );
+ }
+);