diff --git a/examples/index.html b/examples/index.html index 719a72a5e6..ffac167726 100644 --- a/examples/index.html +++ b/examples/index.html @@ -284,9 +284,10 @@

Examples by Role

slider @@ -563,6 +564,7 @@

Examples By Properties and States

  • Navigation Menubar
  • Radio Group Using aria-activedescendant
  • Radio Group Using Roving tabindex
  • +
  • Horizontal Multi-Thumb Slider with Mobile Support
  • Date Picker Spin Button
  • Table
  • Tabs with Automatic Activation
  • @@ -740,9 +742,10 @@

    Examples By Properties and States

    @@ -752,9 +755,10 @@

    Examples By Properties and States

    @@ -765,9 +769,10 @@

    Examples By Properties and States

    @@ -777,8 +782,9 @@

    Examples By Properties and States

    aria-valuetext diff --git a/examples/slider/css/multithumb-slider.css b/examples/slider/css/multithumb-slider.css deleted file mode 100644 index a442e09d2f..0000000000 --- a/examples/slider/css/multithumb-slider.css +++ /dev/null @@ -1,48 +0,0 @@ -/* CSS Document */ - -div.aria-widget-slider { - clear: both; - padding-top: 0.5em; - padding-bottom: 1em; -} - -div.rail-label { - padding-right: 0.5em; - text-align: right; - float: left; - width: 4em; - position: relative; - top: -0.5em; -} - -div.rail-label.max { - padding-left: 0.5em; - text-align: left; -} - -div.aria-widget-slider .rail { - background-color: #eee; - border: 1px solid #888; - position: relative; - height: 4px; - float: left; -} - -div.aria-widget-slider img[role="slider"] { - position: absolute; - padding: 0; - margin: 0; - top: -10px; -} - -div.aria-widget-slider img[role="slider"].focus, -div.aria-widget-slider img[role="slider"]:hover { - outline-color: rgb(140, 203, 242); - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; -} - -div.aria-widget-slider .rail.focus { - background-color: #aaa; -} diff --git a/examples/slider/css/slider-multithumb.css b/examples/slider/css/slider-multithumb.css new file mode 100644 index 0000000000..61d163392b --- /dev/null +++ b/examples/slider/css/slider-multithumb.css @@ -0,0 +1,98 @@ +/* CSS Document */ + +.slider-multithumb { + padding: 6px; + width: 350px; +} + +.slider-multithumb.focus { + padding: 4px; + border: 2px solid #005a9c; + border-radius: 5px; +} + +.slider-multithumb.mobile { + clear: both; + display: grid; + grid-template-columns: 100px 340px 100px; + grid-column-gap: 10px; + width: 550px; +} + +.slider-multithumb.mobile .input-minimum, +.slider-multithumb.mobile .input-maximum { + position: relative; + top: 32px; + height: 1.5em; +} + +.slider-multithumb.mobile .input-minimum { + grid-column: 1; +} + +.slider-multithumb.mobile .input-maximum { + grid-column: 3; +} + +.slider-multithumb.mobile .slider-group { + grid-column: 2; +} + +.slider-multithumb.mobile .input-minimum input, +.slider-multithumb.mobile .input-maximum input { + width: 4em; +} + +.slider-multithumb .slider-group .value { + font-size: 80%; + color: currentColor; + fill: currentColor; +} + +.slider-multithumb .slider-group .rail { + stroke: currentColor; + stroke-width: 2px; + fill: #ccc; +} + +.slider-multithumb .slider-group .thumb { + stroke: transparent; + stroke-width: 2px; + fill: currentColor; +} + +.slider-multithumb .slider-group .focus { + stroke-width: 3px; + stroke: transparent; + fill: transparent; + stroke-linecap: round; + stroke-linejoin: round; +} + +/* Focus and hover styling */ + +.slider-multithumb.focus .slider-group { + color: #005a9c; +} + +.slider-multithumb.focus .slider-group .rail { + stroke: currentColor; + fill: #adddff; +} + +.slider-multithumb [role="slider"]:focus { + outline: none; +} + +.slider-multithumb [role="slider"]:focus .focus { + stroke: currentColor; +} + +.slider-multithumb [role="slider"]:focus .thumb { + stroke: currentColor; +} + +.slider-multithumb [role="slider"]:focus .value { + color: currentColor; + font-weight: bold; +} diff --git a/examples/slider/js/multithumb-slider.js b/examples/slider/js/multithumb-slider.js deleted file mode 100644 index 9f731049da..0000000000 --- a/examples/slider/js/multithumb-slider.js +++ /dev/null @@ -1,235 +0,0 @@ -/* - * This content is licensed according to the W3C Software License at - * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document - * - * File: slider.js - * - * Desc: Slider widget that implements ARIA Authoring Practices - */ - -'use strict'; - -// Create Slider that contains value, valuemin, valuemax, and valuenow -var Slider = function (domNode) { - this.domNode = domNode; - this.railDomNode = domNode.parentNode; - - this.labelDomNode = false; - this.minDomNode = false; - this.maxDomNode = false; - - this.valueNow = 50; - - this.railMin = 0; - this.railMax = 100; - this.railWidth = 0; - this.railBorderWidth = 1; - - this.thumbWidth = 20; - this.thumbHeight = 24; - - this.keyCode = Object.freeze({ - left: 37, - up: 38, - right: 39, - down: 40, - pageUp: 33, - pageDown: 34, - end: 35, - home: 36, - }); -}; - -// Initialize slider -Slider.prototype.init = function () { - if (this.domNode.previousElementSibling) { - this.minDomNode = this.domNode.previousElementSibling; - this.railMin = parseInt(this.minDomNode.getAttribute('aria-valuemin')); - } else { - this.railMin = parseInt(this.domNode.getAttribute('aria-valuemin')); - } - - if (this.domNode.nextElementSibling) { - this.maxDomNode = this.domNode.nextElementSibling; - this.railMax = parseInt(this.maxDomNode.getAttribute('aria-valuemax')); - } else { - this.railMax = parseInt(this.domNode.getAttribute('aria-valuemax')); - } - - this.valueNow = parseInt(this.domNode.getAttribute('aria-valuenow')); - - this.railWidth = parseInt(this.railDomNode.style.width.slice(0, -2)); - - if (this.domNode.classList.contains('min')) { - this.labelDomNode = this.domNode.parentElement.previousElementSibling; - } - - if (this.domNode.classList.contains('max')) { - this.labelDomNode = this.domNode.parentElement.nextElementSibling; - } - - if (this.domNode.tabIndex != 0) { - this.domNode.tabIndex = 0; - } - - this.domNode.addEventListener('keydown', this.handleKeyDown.bind(this)); - this.domNode.addEventListener('mousedown', this.handleMouseDown.bind(this)); - this.domNode.addEventListener('focus', this.handleFocus.bind(this)); - this.domNode.addEventListener('blur', this.handleBlur.bind(this)); - - this.moveSliderTo(this.valueNow); -}; - -Slider.prototype.moveSliderTo = function (value) { - var valueMax = parseInt(this.domNode.getAttribute('aria-valuemax')); - var valueMin = parseInt(this.domNode.getAttribute('aria-valuemin')); - - if (value > valueMax) { - value = valueMax; - } - - if (value < valueMin) { - value = valueMin; - } - - this.valueNow = value; - this.dolValueNow = '$' + value; - - this.domNode.setAttribute('aria-valuenow', this.valueNow); - this.domNode.setAttribute('aria-valuetext', this.dolValueNow); - - if (this.minDomNode) { - this.minDomNode.setAttribute('aria-valuemax', this.valueNow); - } - - if (this.maxDomNode) { - this.maxDomNode.setAttribute('aria-valuemin', this.valueNow); - } - - var pos = Math.round( - ((this.valueNow - this.railMin) * - (this.railWidth - 2 * (this.thumbWidth - this.railBorderWidth))) / - (this.railMax - this.railMin) - ); - - if (this.minDomNode) { - this.domNode.style.left = - pos + this.thumbWidth - this.railBorderWidth + 'px'; - } else { - this.domNode.style.left = pos - this.railBorderWidth + 'px'; - } - - if (this.labelDomNode) { - this.labelDomNode.innerHTML = this.dolValueNow.toString(); - } -}; - -Slider.prototype.handleKeyDown = function (event) { - var flag = false; - - switch (event.keyCode) { - case this.keyCode.left: - case this.keyCode.down: - this.moveSliderTo(this.valueNow - 1); - flag = true; - break; - - case this.keyCode.right: - case this.keyCode.up: - this.moveSliderTo(this.valueNow + 1); - flag = true; - break; - - case this.keyCode.pageDown: - this.moveSliderTo(this.valueNow - 10); - flag = true; - break; - - case this.keyCode.pageUp: - this.moveSliderTo(this.valueNow + 10); - flag = true; - break; - - case this.keyCode.home: - this.moveSliderTo(this.railMin); - flag = true; - break; - - case this.keyCode.end: - this.moveSliderTo(this.railMax); - flag = true; - break; - - default: - break; - } - - if (flag) { - event.preventDefault(); - event.stopPropagation(); - } -}; - -Slider.prototype.handleFocus = function (event) { - this.domNode.classList.add('focus'); - this.railDomNode.classList.add('focus'); -}; - -Slider.prototype.handleBlur = function (event) { - this.domNode.classList.remove('focus'); - this.railDomNode.classList.remove('focus'); -}; - -Slider.prototype.handleMouseDown = function (event) { - var self = this; - - var handleMouseMove = function (event) { - var diffX = event.pageX - self.railDomNode.offsetLeft; - self.valueNow = - self.railMin + - parseInt(((self.railMax - self.railMin) * diffX) / self.railWidth); - self.moveSliderTo(self.valueNow); - - event.preventDefault(); - event.stopPropagation(); - }; - - var handleMouseUp = function (event) { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - - // bind a mousemove event handler to move pointer - document.addEventListener('mousemove', handleMouseMove); - - // bind a mouseup event handler to stop tracking mouse movements - document.addEventListener('mouseup', handleMouseUp); - - event.preventDefault(); - event.stopPropagation(); - - // Set focus to the clicked handle - this.domNode.focus(); -}; - -// handleMouseMove has the same functionality as we need for handleMouseClick on the rail -// Slider.prototype.handleClick = function (event) { - -// var diffX = event.pageX - this.railDomNode.offsetLeft; -// this.valueNow = parseInt(((this.railMax - this.railMin) * diffX) / this.railWidth); -// this.moveSliderTo(this.valueNow); - -// event.preventDefault(); -// event.stopPropagation(); - -// }; - -// Initialize Sliders on the page -window.addEventListener('load', function () { - var sliders = document.querySelectorAll('[role=slider]'); - - for (var i = 0; i < sliders.length; i++) { - var s = new Slider(sliders[i]); - s.init(); - } -}); diff --git a/examples/slider/js/slider-multithumb.js b/examples/slider/js/slider-multithumb.js new file mode 100644 index 0000000000..9d84c568f0 --- /dev/null +++ b/examples/slider/js/slider-multithumb.js @@ -0,0 +1,394 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: slider-multithumb.js + * + * Desc: A dual slider widget that implements ARIA Authoring Practices + */ + +'use strict'; +class SliderMultithumb { + constructor(domNode) { + this.domNode = domNode; + this.svgNode = domNode.querySelector('svg'); + this.railNode = domNode.querySelector('.rail'); + + this.minSliderNode = domNode.querySelector('[role=slider].minimum'); + this.maxSliderNode = domNode.querySelector('[role=slider].maximum'); + + this.minSliderValueNode = this.minSliderNode.querySelector('.value'); + this.maxSliderValueNode = this.maxSliderNode.querySelector('.value'); + + this.minSliderFocusNode = this.minSliderNode.querySelector('.focus'); + this.maxSliderFocusNode = this.maxSliderNode.querySelector('.focus'); + + this.minSliderThumbNode = this.minSliderNode.querySelector('.thumb'); + this.maxSliderThumbNode = this.maxSliderNode.querySelector('.thumb'); + + // The input elements are optional to support mobile devices, + // when a keyboard is not present + this.minInputNode = domNode.querySelector('.input-minimum input'); + this.maxInputNode = domNode.querySelector('.input-maximum input'); + + if (this.minInputNode) { + this.minInputNode.addEventListener( + 'change', + this.onInputChange.bind(this) + ); + this.minInputNode.addEventListener('blur', this.onInputChange.bind(this)); + this.minInputNode.addEventListener('blur', this.onSliderBlur.bind(this)); + this.minInputNode.addEventListener('focus', this.onSliderBlur.bind(this)); + this.minInputNode.min = this.getValueMin(this.minSliderNode); + this.minInputNode.max = this.getValueMax(this.minSliderNode); + } + + if (this.maxInputNode) { + this.maxInputNode.addEventListener( + 'change', + this.onInputChange.bind(this) + ); + this.maxInputNode.addEventListener('blur', this.onInputChange.bind(this)); + this.maxInputNode.addEventListener('blur', this.onSliderBlur.bind(this)); + this.maxInputNode.addEventListener('focus', this.onSliderBlur.bind(this)); + this.maxInputNode.min = this.getValueMin(this.maxSliderNode); + this.maxInputNode.max = this.getValueMax(this.maxSliderNode); + } + + // Dimensions of the slider focus ring, thumb and rail + + this.svgWidth = 360; + this.svgHeight = 80; + + this.valueTop = 24; + this.valueHeight = this.minSliderValueNode.getBoundingClientRect().height; + + this.railHeight = 6; + this.railWidth = 300; + this.railY = 42; + this.railX = 10; + + this.thumbTop = 31; + this.thumbHeight = 28; + this.thumbWidth = 28; + this.thumb2Width = 2 * this.thumbWidth; + this.thumbMiddle = this.thumbTop + this.thumbHeight / 2; + this.thumbBottom = this.thumbTop + this.thumbHeight; + + this.focusOffset = 8; + this.focusY = this.valueTop - this.valueHeight - this.focusOffset + 2; + this.focusWidth = this.thumbWidth + 2 * this.focusOffset; + this.focusHeight = this.thumbBottom - this.focusY + this.focusOffset + 2; + + this.svgNode.setAttribute('width', this.svgWidth); + this.svgNode.setAttribute('height', this.svgHeight); + + this.minSliderFocusNode.setAttribute('y', this.focusY); + this.maxSliderFocusNode.setAttribute('y', this.focusY); + this.minSliderFocusNode.setAttribute('width', this.focusWidth); + this.maxSliderFocusNode.setAttribute('width', this.focusWidth); + this.minSliderFocusNode.setAttribute('height', this.focusHeight); + this.maxSliderFocusNode.setAttribute('height', this.focusHeight); + this.minSliderFocusNode.setAttribute('rx', this.focusWidth / 2); + this.maxSliderFocusNode.setAttribute('rx', this.focusWidth / 2); + + this.minSliderValueNode.setAttribute('y', this.valueTop); + this.maxSliderValueNode.setAttribute('y', this.valueTop); + + this.railNode.setAttribute('y', this.railY); + this.railNode.setAttribute('x', this.railX); + this.railNode.setAttribute('height', this.railHeight); + this.railNode.setAttribute('width', this.railWidth + this.thumbWidth); + this.railNode.setAttribute('rx', this.railHeight / 2); + + this.sliderMinValue = this.getValueMin(this.minSliderNode); + this.sliderMaxValue = this.getValueMax(this.maxSliderNode); + + this.minSliderRight = 0; + this.maxSliderLeft = this.railWidth; + + this.minSliderNode.addEventListener( + 'keydown', + this.onSliderKeydown.bind(this) + ); + this.minSliderNode.addEventListener( + 'mousedown', + this.onSliderMousedown.bind(this) + ); + this.minSliderNode.addEventListener('focus', this.onSliderFocus.bind(this)); + this.minSliderNode.addEventListener('blur', this.onSliderBlur.bind(this)); + + this.maxSliderNode.addEventListener( + 'keydown', + this.onSliderKeydown.bind(this) + ); + this.maxSliderNode.addEventListener( + 'mousedown', + this.onSliderMousedown.bind(this) + ); + this.maxSliderNode.addEventListener('focus', this.onSliderFocus.bind(this)); + this.maxSliderNode.addEventListener('blur', this.onSliderBlur.bind(this)); + + this.moveSliderTo(this.minSliderNode, this.getValue(this.minSliderNode)); + + this.moveSliderTo(this.maxSliderNode, this.getValue(this.maxSliderNode)); + } + + getValue(sliderNode) { + return parseInt(sliderNode.getAttribute('aria-valuenow')); + } + + getValueMin(sliderNode) { + return parseInt(sliderNode.getAttribute('aria-valuemin')); + } + + getValueMax(sliderNode) { + return parseInt(sliderNode.getAttribute('aria-valuemax')); + } + + isMinSlider(sliderNode) { + return this.minSliderNode === sliderNode; + } + + isMinInput(inputNode) { + return this.minInputNode === inputNode; + } + + isInRange(sliderNode, value) { + let valueMin = this.getValueMin(sliderNode); + let valueMax = this.getValueMax(sliderNode); + return value <= valueMax && value >= valueMin; + } + + isOutOfRange(value) { + let valueMin = this.getValueMin(this.minSliderNode); + let valueMax = this.getValueMax(this.maxSliderNode); + return value > valueMax || value < valueMin; + } + + moveSliderTo(sliderNode, value) { + var valueMax, + valueMin, + pos, + x, + points = '', + width; + + if (this.isMinSlider(sliderNode)) { + valueMin = this.getValueMin(this.minSliderNode); + valueMax = this.getValueMax(this.minSliderNode); + } else { + valueMin = this.getValueMin(this.maxSliderNode); + valueMax = this.getValueMax(this.maxSliderNode); + } + + if (value > valueMax) { + value = valueMax; + } + + if (value < valueMin) { + value = valueMin; + } + + var dollarValue = '$' + value; + sliderNode.setAttribute('aria-valuenow', value); + sliderNode.setAttribute('aria-valuetext', dollarValue); + + pos = this.railX; + pos += Math.round( + (value * (this.railWidth - this.thumbWidth)) / + (this.sliderMaxValue - this.sliderMinValue) + ); + + if (this.isMinSlider(sliderNode)) { + // update INPUT, label and ARIA attributes + this.minSliderValueNode.textContent = dollarValue; + this.maxSliderNode.setAttribute('aria-valuemin', value); + + if (this.maxInputNode && this.minInputNode) { + this.maxInputNode.min = value; + this.minInputNode.value = value; + } + + // move the SVG focus ring and thumb elements + x = pos - this.focusOffset - 1; + this.minSliderFocusNode.setAttribute('x', x); + + points = `${pos},${this.thumbTop}`; + points += ` ${pos + this.thumbWidth},${this.thumbMiddle}`; + points += ` ${pos},${this.thumbBottom}`; + this.minSliderThumbNode.setAttribute('points', points); + + // Position value + width = this.minSliderValueNode.getBoundingClientRect().width; + pos = pos + (this.thumbWidth - width) / 2; + if (pos + width > this.maxSliderLeft - 2) { + pos = this.maxSliderLeft - width - 2; + } + this.minSliderValueNode.setAttribute('x', pos); + this.minSliderRight = pos; + } else { + // move the SVG focus ring and thumb elements + x = pos + this.thumbWidth - this.focusOffset + 1; + this.maxSliderFocusNode.setAttribute('x', x); + + points = `${pos + this.thumbWidth},${this.thumbMiddle}`; + points += ` ${pos + this.thumb2Width},${this.thumbTop}`; + points += ` ${pos + this.thumb2Width},${this.thumbBottom}`; + this.maxSliderThumbNode.setAttribute('points', points); + + width = this.maxSliderValueNode.getBoundingClientRect().width; + pos = pos + this.thumbWidth + (this.thumbWidth - width) / 2; + if (pos - width < this.minSliderRight + 2) { + pos = this.minSliderRight + width + 2; + } + this.maxSliderValueNode.setAttribute('x', pos); + this.maxSliderLeft = pos; + + // update INPUT, label and ARIA attributes + this.maxSliderValueNode.textContent = dollarValue; + this.minSliderNode.setAttribute('aria-valuemax', value); + + if (this.maxInputNode && this.minInputNode) { + this.minInputNode.max = value; + this.maxInputNode.value = value; + } + } + } + + onSliderKeydown(event) { + var flag = false; + var sliderNode = event.currentTarget; + var value = this.getValue(sliderNode); + var valueMin = this.getValueMin(sliderNode); + var valueMax = this.getValueMax(sliderNode); + + switch (event.key) { + case 'ArrowLeft': + case 'ArrowDown': + this.moveSliderTo(sliderNode, value - 1); + flag = true; + break; + + case 'ArrowRight': + case 'ArrowUp': + this.moveSliderTo(sliderNode, value + 1); + flag = true; + break; + + case 'PageDown': + this.moveSliderTo(sliderNode, value - 10); + flag = true; + break; + + case 'PageUp': + this.moveSliderTo(sliderNode, value + 10); + flag = true; + break; + + case 'Home': + this.moveSliderTo(sliderNode, valueMin); + flag = true; + break; + + case 'End': + this.moveSliderTo(sliderNode, valueMax); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.preventDefault(); + event.stopPropagation(); + } + } + + onSliderFocus() { + this.domNode.classList.add('focus'); + } + + onSliderBlur() { + this.domNode.classList.remove('focus'); + } + + onSliderMousedown(event) { + var onMousemove = function (e) { + var diffX = e.pageX - this.railNode.getBoundingClientRect().left; + + if (isMinSlider) { + diffX -= this.thumbWidth / 2; + } else { + diffX -= (3 * this.thumbWidth) / 2; + } + + var value = parseInt( + ((this.sliderMaxValue - this.sliderMinValue) * diffX) / this.railWidth + ); + + this.moveSliderTo(sliderNode, value); + + e.preventDefault(); + e.stopPropagation(); + }.bind(this); + + var onMouseup = function () { + document.removeEventListener('mousemove', onMousemove); + document.removeEventListener('mouseup', onMouseup); + }; + + var sliderNode = event.currentTarget; + var isMinSlider = this.isMinSlider(sliderNode); + + // bind a mousemove event handler to move pointer + document.addEventListener('mousemove', onMousemove); + + // bind a mouseup event handler to stop tracking mouse movements + document.addEventListener('mouseup', onMouseup); + + event.preventDefault(); + event.stopPropagation(); + + // Set focus to the clicked handle + sliderNode.focus(); + } + + onInputChange(event) { + var tgt = event.currentTarget, + value = tgt.value, + isNumber = typeof parseInt(value) === 'number'; + + if (this.isMinInput(tgt)) { + if (value.length === 0) { + tgt.value = this.getValue(this.minSliderNode); + } else { + if (isNumber && this.isInRange(this.minSliderNode, value)) { + this.moveSliderTo(this.minSliderNode, value); + } else { + tgt.value = this.getValue(this.minSliderNode); + } + } + } else { + if (value.length === 0) { + tgt.value = this.getValue(this.maxSliderNode); + } else { + if (isNumber && this.isInRange(this.maxSliderNode, value)) { + this.moveSliderTo(this.maxSliderNode, value); + } else { + tgt.value = this.getValue(this.maxSliderNode); + } + } + } + } +} + +// Initialize Multithumb Slider widgets on the page +window.addEventListener('load', function () { + var slidersMultithumb = document.querySelectorAll('.slider-multithumb'); + + for (let i = 0; i < slidersMultithumb.length; i++) { + new SliderMultithumb(slidersMultithumb[i]); + } +}); diff --git a/examples/slider/multithumb-slider.html b/examples/slider/multithumb-slider.html deleted file mode 100644 index a40edc524d..0000000000 --- a/examples/slider/multithumb-slider.html +++ /dev/null @@ -1,277 +0,0 @@ - - - - - Horizontal Multi-Thumb Slider Example | WAI-ARIA Authoring Practices 1.2 - - - - - - - - - - - - - - -
    -

    Horizontal Multi-Thumb Slider Example

    -

    - The below example section includes a slider for setting a price range that demonstrates the - multi-thumb slider design pattern. - Users set a price range by moving the arrows (thumbs). - Each slider has two thumbs: one for the minimum price and one for the maximum price. - The price labels at the ends of the slider update as their corresponding thumbs are moved. -

    -

    Similar examples include:

    - -
    -
    -

    Example

    -
    - -
    -

    Hotel Price Range

    -
    -
    - 0 -
    -
    - - -
    -
    - 0 -
    -
    -

    Flight Price Range

    -
    -
    - 0 -
    -
    - - -
    -
    - 0 -
    -
    -
    - -
    - -
    -

    Keyboard Support

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    KeyFunction
    Right ArrowIncreases slider value one step.
    Up ArrowIncreases slider value one step.
    Left ArrowDecreases slider value one step.
    Down ArrowDecreases slider value one step.
    Page UpIncreases slider value multiple steps. In this slider, jumps ten steps.
    Page DownDecreases slider value multiple steps. In this slider, jumps ten steps.
    HomeSets slider to its minimum value.
    EndSets slider to its maximum value.
    -
    - -
    -

    Role, Property, State, and Tabindex Attributes

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    RoleAttributeElementUsage
    - slider - - img - -
      -
    • Identifies the img element as a slider.
    • -
    • Set on the movable thumb because it is the operable element that receives focus and represents the slider value.
    • -
    -
    - tabindex=0 - - img - Includes the slider thumb in the page tab sequence.
    - aria-valuemax=NUMBER - - img - Specifies the maximum value of the slider.
    - aria-valuemin=NUMBER - - img - Specifies the minimum value of the slider.
    - aria-valuenow=NUMBER - - img - Indicates the current value of the slider.
    - aria-valuetext=DOLLAR AMOUNT - - img - Indicates the current value of the slider in dollars using the $ character as a prefix.
    - aria-label=text - - img - A label identifying the purpose of the slider, e.g., Hotel Minimum Price.
    -
    - -
    -

    Javascript and CSS Source Code

    - -
    - -
    -

    HTML Source Code

    - -
    - - -
    - -
    - - - diff --git a/examples/slider/slider-multithumb-mobile.html b/examples/slider/slider-multithumb-mobile.html new file mode 100644 index 0000000000..68efa03923 --- /dev/null +++ b/examples/slider/slider-multithumb-mobile.html @@ -0,0 +1,354 @@ + + + + + Horizontal Multi-Thumb Slider Example with Mobile Support | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + +
    +

    Horizontal Multi-Thumb Slider Example with Mobile Support

    +

    + The below example section includes a slider for setting a price range that demonstrates the + multi-thumb slider design pattern. + Users set a price range by moving the arrows (thumbs). + Each slider has two thumbs: one for the minimum price and one for the maximum price. + The price labels at the ends of the slider update as their corresponding thumbs are moved. +

    +

    Similar examples include:

    + +
    +
    +

    Example

    +
    + +
    +

    Hotel Price Range

    +
    + + $ + + + + + + + 0 + + + + + 0 + + + + + + $ + + +
    +

    Flight Price Range

    +
    + + $ + + + + + + 0 + + + + + 0 + + + + + + $ + + +
    +
    + +
    + +
    +

    Accessibility Features

    + +
    + +
    +

    Keyboard Support

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyFunction
    Right ArrowIncreases slider value one step.
    Up ArrowIncreases slider value one step.
    Left ArrowDecreases slider value one step.
    Down ArrowDecreases slider value one step.
    Page UpIncreases slider value multiple steps. In this slider, jumps ten steps.
    Page DownDecreases slider value multiple steps. In this slider, jumps ten steps.
    HomeSets slider to its minimum value.
    EndSets slider to its maximum value.
    +
    +
    +

    Role, Property, State, and Tabindex Attributes

    +

    Sliders

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    + slider + + img + +
      +
    • Identifies the img element as a slider.
    • +
    • Set on the movable thumb because it is the operable element that receives focus and represents the slider value.
    • +
    +
    + tabindex=0 + + img + Includes the slider thumb in the page tab sequence.
    + aria-valuemax=NUMBER + + img + Specifies the maximum value of the slider.
    + aria-valuemin=NUMBER + + img + Specifies the minimum value of the slider.
    + aria-valuenow=NUMBER + + img + Indicates the current value of the slider.
    + aria-valuetext=DOLLAR AMOUNT + + img + Indicates the current value of the slider in dollars using the $ character as a prefix.
    + aria-label=text + + img + A label identifying the purpose of the slider, e.g., Hotel Minimum Price.
    + +

    Input

    + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    + tabindex=-1 + + input[type=number] + When a keyboard is being used users can use keyboard commands to change the value of the slider widget and do not need to use the input controls, so they are removed from the tab sequence of the page.
    +
    +
    +

    Javascript and CSS Source Code

    + +
    +
    +

    HTML Source Code

    + +
    + + +
    +
    + + + diff --git a/examples/slider/slider-multithumb.html b/examples/slider/slider-multithumb.html new file mode 100644 index 0000000000..8ce1e57ba0 --- /dev/null +++ b/examples/slider/slider-multithumb.html @@ -0,0 +1,287 @@ + + + + + Horizontal Multi-Thumb Slider Example | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + +
    +

    Horizontal Multi-Thumb Slider Example

    +

    + The below example section includes a slider for setting a price range that demonstrates the + multi-thumb slider design pattern. + Users set a price range by moving the arrows (thumbs). + Each slider has two thumbs: one for the minimum price and one for the maximum price. + The price labels at the ends of the slider update as their corresponding thumbs are moved. +

    +

    Similar examples include:

    + +
    +
    +

    Example

    +
    + +
    +

    Hotel Price Range

    +
    + + + + 0 + + + + + 0 + + + + +
    +

    Flight Price Range

    +
    + + + + 0 + + + + + 0 + + + + +
    +
    + +
    + +
    +

    Accessibility Features

    + +
    + +
    +

    Keyboard Support

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyFunction
    Right ArrowIncreases slider value one step.
    Up ArrowIncreases slider value one step.
    Left ArrowDecreases slider value one step.
    Down ArrowDecreases slider value one step.
    Page UpIncreases slider value multiple steps. In this slider, jumps ten steps.
    Page DownDecreases slider value multiple steps. In this slider, jumps ten steps.
    HomeSets slider to its minimum value.
    EndSets slider to its maximum value.
    +
    +
    +

    Role, Property, State, and Tabindex Attributes

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    + slider + + img + +
      +
    • Identifies the img element as a slider.
    • +
    • Set on the movable thumb because it is the operable element that receives focus and represents the slider value.
    • +
    +
    + tabindex=0 + + img + Includes the slider thumb in the page tab sequence.
    + aria-valuemax=NUMBER + + img + Specifies the maximum value of the slider.
    + aria-valuemin=NUMBER + + img + Specifies the minimum value of the slider.
    + aria-valuenow=NUMBER + + img + Indicates the current value of the slider.
    + aria-valuetext=DOLLAR AMOUNT + + img + Indicates the current value of the slider in dollars using the $ character as a prefix.
    + aria-label=text + + img + A label identifying the purpose of the slider, e.g., Hotel Minimum Price.
    +
    +
    +

    Javascript and CSS Source Code

    + +
    +
    +

    HTML Source Code

    + +
    + + +
    +
    + + + diff --git a/test/tests/slider_slider-multithumb-mobile.js b/test/tests/slider_slider-multithumb-mobile.js new file mode 100644 index 0000000000..8872fd2edf --- /dev/null +++ b/test/tests/slider_slider-multithumb-mobile.js @@ -0,0 +1,1163 @@ +const { ariaTest } = require('..'); +const { Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); +const assertAriaLabelExists = require('../util/assertAriaLabelExists'); +const assertAriaRoles = require('../util/assertAriaRoles'); + +const exampleFile = 'slider/slider-multithumb-mobile.html'; + +const ex = { + inputSelector: '#ex1 input[type="number"]', + sliderSelector: '#ex1 [role="slider"]', + hotelSliderSelector: '#ex1 .slider-multithumb:nth-of-type(1) [role="slider"]', + flightSliderSelector: + '#ex1 .slider-multithumb:nth-of-type(2) [role="slider"]', + hotelMin: '0', + hotelMax: '400', + flightMin: '0', + flightMax: '1000', + hotelDefaultValues: ['100', '250'], + flightDefaultValues: ['100', '250'], + hotelLabelSelector: + '#ex1 .slider-multithumb:nth-of-type(1) [role=slider] .value', + flightLabelSelector: + '#ex1 .slider-multithumb:nth-of-type(2) [role=slider] .value', +}; + +const verifyAllValues = async function ( + t, + value, + slider1, + attribute1, + slider2, + attribute2, + label, + message +) { + t.is( + await slider1.getAttribute(attribute1), + value, + attribute1 + ' on first slider: ' + message + ); + + t.is( + await slider2.getAttribute(attribute2), + value, + attribute2 + ' on second slider: ' + message + ); + + t.is(await label.getText(), '$' + value, 'value in label after: ' + message); +}; + +// Attributes + +ariaTest( + '"tabindex" set to "-1" on input elements', + exampleFile, + 'input-tabindex', + async (t) => { + await assertAttributeValues(t, ex.inputSelector, 'tabindex', '-1'); + } +); + +ariaTest( + 'role="slider" on div element', + exampleFile, + 'slider-role', + async (t) => { + await assertAriaRoles(t, 'ex1', 'slider', '4', 'g'); + } +); + +ariaTest( + '"tabindex" set to "0" on sliders', + exampleFile, + 'tabindex', + async (t) => { + await assertAttributeValues(t, ex.sliderSelector, 'tabindex', '0'); + } +); + +ariaTest( + '"aria-valuemax" set on sliders', + exampleFile, + 'aria-valuemax', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + + t.is( + await hotelSliders[0].getAttribute('aria-valuemax'), + ex.hotelDefaultValues[1], + 'Value of "aria-valuemax" for first hotel slider on page load should be: ' + + ex.hotelDefaultValues[1] + ); + t.is( + await hotelSliders[1].getAttribute('aria-valuemax'), + ex.hotelMax, + 'Value of "aria-valuemax" for second hotel slider on page load should be: ' + + ex.hotelMax + ); + t.is( + await flightSliders[0].getAttribute('aria-valuemax'), + ex.flightDefaultValues[1], + 'Value of "aria-valuemax" for first flight slider on page load should be: ' + + ex.flightDefaultValues[1] + ); + + t.is( + await flightSliders[1].getAttribute('aria-valuemax'), + ex.flightMax, + 'Value of "aria-valuemax" for second flight slider on page load should be: ' + + ex.flightMax + ); + } +); + +ariaTest( + '"aria-valuemin" set to "0" on sliders', + exampleFile, + 'aria-valuemin', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + + t.is( + await hotelSliders[0].getAttribute('aria-valuemin'), + '0', + 'Value of "aria-valuemin" for first hotel slider on page load should be: "0"' + ); + t.is( + await hotelSliders[1].getAttribute('aria-valuemin'), + ex.hotelDefaultValues[0], + 'Value of "aria-valuemin" for second hotel slider on page load should be: ' + + ex.hotelDefaultValues[0] + ); + t.is( + await flightSliders[0].getAttribute('aria-valuemin'), + '0', + 'Value of "aria-valuemin" for first flight slider on page load should be: "0"' + ); + t.is( + await flightSliders[1].getAttribute('aria-valuemin'), + ex.flightDefaultValues[0], + 'Value of "aria-valuemin" for second flight slider on page load should be: ' + + ex.flightDefaultValues[0] + ); + } +); + +ariaTest( + '"aria-valuenow" reflects slider value', + exampleFile, + 'aria-valuenow', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + + t.is( + await hotelSliders[0].getAttribute('aria-valuenow'), + ex.hotelDefaultValues[0], + 'Value of "aria-valuenow" for first hotel slider on page load should be: ' + + ex.hotelDefaultValues[0] + ); + t.is( + await hotelSliders[1].getAttribute('aria-valuenow'), + ex.hotelDefaultValues[1], + 'Value of "aria-valuenow" for second hotel slider on page load should be: ' + + ex.hotelDefaultValues[1] + ); + t.is( + await flightSliders[0].getAttribute('aria-valuenow'), + ex.flightDefaultValues[0], + 'Value of "aria-valuenow" for first flight slider on page load should be: ' + + ex.flightDefaultValues[0] + ); + t.is( + await flightSliders[1].getAttribute('aria-valuenow'), + ex.flightDefaultValues[1], + 'Value of "aria-valuenow" for second flight slider on page load should be: ' + + ex.flightDefaultValues[1] + ); + } +); + +ariaTest( + '"aria-valuetext" reflects slider value', + exampleFile, + 'aria-valuetext', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + + t.is( + await hotelSliders[0].getAttribute('aria-valuetext'), + '$' + ex.hotelDefaultValues[0], + 'Value of "aria-valuetext" for first hotel slider on page load should be: $' + + ex.hotelDefaultValues[0] + ); + t.is( + await hotelSliders[1].getAttribute('aria-valuetext'), + '$' + ex.hotelDefaultValues[1], + 'Value of "aria-valuetext" for second hotel slider on page load should be: $' + + ex.hotelDefaultValues[1] + ); + t.is( + await flightSliders[0].getAttribute('aria-valuetext'), + '$' + ex.flightDefaultValues[0], + 'Value of "aria-valuetext" for first flight slider on page load should be: $' + + ex.flightDefaultValues[0] + ); + t.is( + await flightSliders[1].getAttribute('aria-valuetext'), + '$' + ex.flightDefaultValues[1], + 'Value of "aria-valuetext" for second flight slider on page load should be: $' + + ex.flightDefaultValues[1] + ); + } +); + +ariaTest( + '"aria-label" set on sliders', + exampleFile, + 'aria-label', + async (t) => { + await assertAriaLabelExists(t, ex.sliderSelector); + } +); + +// Keys + +ariaTest( + 'Right arrow increases slider value by 1', + exampleFile, + 'key-right-arrow', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + // Send 1 key to lower hotel slider + await hotelSliders[0].sendKeys(Key.ARROW_RIGHT); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[0]) + 1).toString(), + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one ARROW RIGHT to lower hotel slider' + ); + + await hotelSliders[0].sendKeys(Key.END, Key.ARROW_RIGHT); + + await verifyAllValues( + t, + ex.hotelDefaultValues[1], + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after END then one ARROW RIGHT to lower hotel slider' + ); + + // Send 1 key to lower upper slider + await hotelSliders[1].sendKeys(Key.ARROW_RIGHT); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[1]) + 1).toString(), + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one ARROW RIGHT to lower hotel slider' + ); + + await hotelSliders[1].sendKeys(Key.END, Key.ARROW_RIGHT); + + await verifyAllValues( + t, + ex.hotelMax, + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after END then one ARROW RIGHT to upper hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements( + t, + ex.flightLabelSelector + ); + + // Send 1 key to lower flight slider + await flightSliders[0].sendKeys(Key.ARROW_RIGHT); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[0]) + 1).toString(), + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after one ARROW RIGHT to lower flight slider' + ); + + await flightSliders[0].sendKeys(Key.END, Key.ARROW_RIGHT); + + await verifyAllValues( + t, + ex.flightDefaultValues[1], + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after END then one ARROW RIGHT to lower flight slider' + ); + + // Send 1 key to lower upper slider + await flightSliders[1].sendKeys(Key.ARROW_RIGHT); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[1]) + 1).toString(), + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one ARROW RIGHT to lower flight slider' + ); + + await flightSliders[1].sendKeys(Key.END, Key.ARROW_RIGHT); + + await verifyAllValues( + t, + ex.flightMax, + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after END then one ARROW RIGHT to upper flight slider' + ); + } +); + +ariaTest( + 'Up arrow increases slider value by 1', + exampleFile, + 'key-up-arrow', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + // Send 1 key to lower hotel slider + await hotelSliders[0].sendKeys(Key.ARROW_UP); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[0]) + 1).toString(), + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one ARROW UP to lower hotel slider' + ); + + await hotelSliders[0].sendKeys(Key.END, Key.ARROW_UP); + + await verifyAllValues( + t, + ex.hotelDefaultValues[1], + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after END then one ARROW UP to lower hotel slider' + ); + + // Send 1 key to lower upper slider + await hotelSliders[1].sendKeys(Key.ARROW_UP); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[1]) + 1).toString(), + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one ARROW UP to lower hotel slider' + ); + + await hotelSliders[1].sendKeys(Key.END, Key.ARROW_UP); + + await verifyAllValues( + t, + ex.hotelMax, + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after END then one ARROW UP to upper hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements( + t, + ex.flightLabelSelector + ); + + // Send 1 key to lower flight slider + await flightSliders[0].sendKeys(Key.ARROW_UP); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[0]) + 1).toString(), + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after one ARROW UP to lower flight slider' + ); + + await flightSliders[0].sendKeys(Key.END, Key.ARROW_UP); + + await verifyAllValues( + t, + ex.flightDefaultValues[1], + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after END then one ARROW UP to lower flight slider' + ); + + // Send 1 key to lower upper slider + await flightSliders[1].sendKeys(Key.ARROW_UP); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[1]) + 1).toString(), + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one ARROW UP to lower flight slider' + ); + + await flightSliders[1].sendKeys(Key.END, Key.ARROW_UP); + + await verifyAllValues( + t, + ex.flightMax, + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after END then one ARROW UP to upper flight slider' + ); + } +); + +ariaTest( + 'Page up increases slider value by 10', + exampleFile, + 'key-page-up', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + // Send 1 key to lower hotel slider + await hotelSliders[0].sendKeys(Key.PAGE_UP); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[0]) + 10).toString(), + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one PAGE UP to lower hotel slider' + ); + + await hotelSliders[0].sendKeys(Key.END, Key.PAGE_UP); + + await verifyAllValues( + t, + ex.hotelDefaultValues[1], + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after END then one PAGE UP to lower hotel slider' + ); + + // Send 1 key to lower upper slider + await hotelSliders[1].sendKeys(Key.PAGE_UP); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[1]) + 10).toString(), + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one PAGE UP to upper hotel slider' + ); + + await hotelSliders[1].sendKeys(Key.END, Key.PAGE_UP); + + await verifyAllValues( + t, + ex.hotelMax, + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after END then one PAGE UP to upper hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements( + t, + ex.flightLabelSelector + ); + + // Send 1 key to lower flight slider + await flightSliders[0].sendKeys(Key.PAGE_UP); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[0]) + 10).toString(), + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after one PAGE UP to lower flight slider' + ); + + await flightSliders[0].sendKeys(Key.END, Key.PAGE_UP); + + await verifyAllValues( + t, + ex.flightDefaultValues[1], + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after END then one PAGE UP to lower flight slider' + ); + + // Send 1 key to lower upper slider + await flightSliders[1].sendKeys(Key.PAGE_UP); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[1]) + 10).toString(), + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one PAGE UP to upper flight slider' + ); + + await flightSliders[1].sendKeys(Key.END, Key.PAGE_UP); + + await verifyAllValues( + t, + ex.flightMax, + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after END then one PAGE UP to upper flight slider' + ); + } +); + +ariaTest( + 'left arrow decreases slider value by 1', + exampleFile, + 'key-left-arrow', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + // Send 1 key to lower hotel slider + await hotelSliders[0].sendKeys(Key.ARROW_LEFT); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[0]) - 1).toString(), + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one ARROW LEFT to lower hotel slider' + ); + + await hotelSliders[0].sendKeys(Key.HOME, Key.ARROW_LEFT); + + await verifyAllValues( + t, + ex.hotelMin, + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after HOME then one ARROW LEFT to lower hotel slider' + ); + + // Send 1 key to lower upper slider + await hotelSliders[1].sendKeys(Key.ARROW_LEFT); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[1]) - 1).toString(), + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one ARROW LEFT to upper hotel slider' + ); + + await hotelSliders[1].sendKeys(Key.HOME, Key.ARROW_LEFT); + + await verifyAllValues( + t, + ex.hotelMin, + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after HOME then one ARROW LEFT to upper hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements( + t, + ex.flightLabelSelector + ); + + // Send 1 key to lower flight slider + await flightSliders[0].sendKeys(Key.ARROW_LEFT); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[0]) - 1).toString(), + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after one ARROW LEFT to lower flight slider' + ); + + await flightSliders[0].sendKeys(Key.HOME, Key.ARROW_LEFT); + + await verifyAllValues( + t, + ex.flightMin, + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after HOME then one ARROW LEFT to lower flight slider' + ); + + // Send 1 key to lower upper slider + await flightSliders[1].sendKeys(Key.ARROW_LEFT); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[1]) - 1).toString(), + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one ARROW LEFT to upper flight slider' + ); + + await flightSliders[1].sendKeys(Key.HOME, Key.ARROW_LEFT); + + await verifyAllValues( + t, + ex.flightMin, + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after HOME then one ARROW LEFT to upper flight slider' + ); + } +); + +ariaTest( + 'down arrow decreases slider value by 1', + exampleFile, + 'key-down-arrow', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + // Send 1 key to lower hotel slider + await hotelSliders[0].sendKeys(Key.ARROW_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[0]) - 1).toString(), + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one ARROW DOWN to lower hotel slider' + ); + + await hotelSliders[0].sendKeys(Key.HOME, Key.ARROW_DOWN); + + await verifyAllValues( + t, + ex.hotelMin, + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after HOME then one ARROW DOWN to lower hotel slider' + ); + + // Send 1 key to lower upper slider + await hotelSliders[1].sendKeys(Key.ARROW_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[1]) - 1).toString(), + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one ARROW DOWN to upper hotel slider' + ); + + await hotelSliders[1].sendKeys(Key.HOME, Key.ARROW_DOWN); + + await verifyAllValues( + t, + ex.hotelMin, + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after HOME then one ARROW DOWN to upper hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements( + t, + ex.flightLabelSelector + ); + + // Send 1 key to lower flight slider + await flightSliders[0].sendKeys(Key.ARROW_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[0]) - 1).toString(), + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after one ARROW DOWN to lower flight slider' + ); + + await flightSliders[0].sendKeys(Key.HOME, Key.ARROW_DOWN); + + await verifyAllValues( + t, + ex.flightMin, + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after HOME then one ARROW DOWN to lower flight slider' + ); + + // Send 1 key to lower upper slider + await flightSliders[1].sendKeys(Key.ARROW_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[1]) - 1).toString(), + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one ARROW DOWN to upper flight slider' + ); + + await flightSliders[1].sendKeys(Key.HOME, Key.ARROW_DOWN); + + await verifyAllValues( + t, + ex.flightMin, + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after HOME then one ARROW DOWN to upper flight slider' + ); + } +); + +ariaTest( + 'page down decreases slider value by 10', + exampleFile, + 'key-page-down', + async (t) => { + const hotelSliders = await t.context.queryElements( + t, + ex.hotelSliderSelector + ); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + // Send 1 key to lower hotel slider + await hotelSliders[0].sendKeys(Key.PAGE_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[0]) - 10).toString(), + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one PAGE DOWN to lower hotel slider' + ); + + await hotelSliders[0].sendKeys(Key.HOME, Key.PAGE_DOWN); + + await verifyAllValues( + t, + ex.hotelMin, + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after HOME then one PAGE DOWN to lower hotel slider' + ); + + // Send 1 key to lower upper slider + await hotelSliders[1].sendKeys(Key.PAGE_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.hotelDefaultValues[1]) - 10).toString(), + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one PAGE DOWN to upper hotel slider' + ); + + await hotelSliders[1].sendKeys(Key.HOME, Key.PAGE_DOWN); + + await verifyAllValues( + t, + ex.hotelMin, + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after HOME then one PAGE DOWN to upper hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements( + t, + ex.flightLabelSelector + ); + + // Send 1 key to lower flight slider + await flightSliders[0].sendKeys(Key.PAGE_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[0]) - 10).toString(), + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after one PAGE DOWN to lower flight slider' + ); + + await flightSliders[0].sendKeys(Key.HOME, Key.PAGE_DOWN); + + await verifyAllValues( + t, + ex.flightMin, + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after HOME then one PAGE DOWN to lower flight slider' + ); + + // Send 1 key to lower upper slider + await flightSliders[1].sendKeys(Key.PAGE_DOWN); + + await verifyAllValues( + t, + (parseInt(ex.flightDefaultValues[1]) - 10).toString(), + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one PAGE DOWN to lower flight slider' + ); + + await flightSliders[1].sendKeys(Key.HOME, Key.PAGE_DOWN); + + await verifyAllValues( + t, + ex.flightMin, + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after HOME then one PAGE DOWN to upper flight slider' + ); + } +); + +ariaTest('home sends value to minimum', exampleFile, 'key-home', async (t) => { + const hotelSliders = await t.context.queryElements(t, ex.hotelSliderSelector); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + // Send 1 key to upper hotel slider + await hotelSliders[1].sendKeys(Key.HOME); + + await verifyAllValues( + t, + ex.hotelDefaultValues[0], + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one HOME to upper hotel slider' + ); + + // Send 1 key to upper hotel slider + await hotelSliders[0].sendKeys(Key.HOME); + + await verifyAllValues( + t, + ex.hotelMin, + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one HOME to lower hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements(t, ex.flightLabelSelector); + + // Send 1 key to upper flight slider + await flightSliders[1].sendKeys(Key.HOME); + + await verifyAllValues( + t, + ex.flightDefaultValues[0], + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one HOME to upper flight slider' + ); + + // Send 1 key to upper flight slider + await flightSliders[0].sendKeys(Key.HOME); + + await verifyAllValues( + t, + ex.flightMin, + flightSliders[0], + 'aria-valuenow', + flightSliders[1], + 'aria-valuemin', + flightLabels[0], + 'after one HOME to lower flight slider' + ); +}); + +ariaTest('end sends value to minimum', exampleFile, 'key-end', async (t) => { + const hotelSliders = await t.context.queryElements(t, ex.hotelSliderSelector); + const hotelLabels = await t.context.queryElements(t, ex.hotelLabelSelector); + + await hotelSliders[0].sendKeys(Key.END); + + await verifyAllValues( + t, + ex.hotelDefaultValues[1], + hotelSliders[0], + 'aria-valuenow', + hotelSliders[1], + 'aria-valuemin', + hotelLabels[0], + 'after one END to lower hotel slider' + ); + + await hotelSliders[1].sendKeys(Key.END); + + await verifyAllValues( + t, + ex.hotelMax, + hotelSliders[0], + 'aria-valuemax', + hotelSliders[1], + 'aria-valuenow', + hotelLabels[1], + 'after one END to upper hotel slider' + ); + + const flightSliders = await t.context.queryElements( + t, + ex.flightSliderSelector + ); + const flightLabels = await t.context.queryElements(t, ex.flightLabelSelector); + + await flightSliders[0].sendKeys(Key.END); + + await verifyAllValues( + t, + ex.flightDefaultValues[1], + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[0], + 'after one END to lower flight slider' + ); + + await flightSliders[1].sendKeys(Key.END); + + await verifyAllValues( + t, + ex.flightMax, + flightSliders[0], + 'aria-valuemax', + flightSliders[1], + 'aria-valuenow', + flightLabels[1], + 'after one END to upper flight slider' + ); +}); diff --git a/test/tests/slider_multithumb.js b/test/tests/slider_slider-multithumb.js similarity index 98% rename from test/tests/slider_multithumb.js rename to test/tests/slider_slider-multithumb.js index 46e6f47827..abd8afbe5a 100644 --- a/test/tests/slider_multithumb.js +++ b/test/tests/slider_slider-multithumb.js @@ -4,22 +4,23 @@ const assertAttributeValues = require('../util/assertAttributeValues'); const assertAriaLabelExists = require('../util/assertAriaLabelExists'); const assertAriaRoles = require('../util/assertAriaRoles'); -const exampleFile = 'slider/multithumb-slider.html'; +const exampleFile = 'slider/slider-multithumb.html'; const ex = { sliderSelector: '#ex1 [role="slider"]', - hotelSliderSelector: - '#ex1 div.aria-widget-slider:nth-of-type(1) [role="slider"]', + hotelSliderSelector: '#ex1 .slider-multithumb:nth-of-type(1) [role="slider"]', flightSliderSelector: - '#ex1 div.aria-widget-slider:nth-of-type(2) [role="slider"]', + '#ex1 .slider-multithumb:nth-of-type(2) [role="slider"]', hotelMin: '0', hotelMax: '400', flightMin: '0', flightMax: '1000', hotelDefaultValues: ['100', '250'], flightDefaultValues: ['100', '250'], - hotelLabelSelector: '#ex1 div.aria-widget-slider:nth-of-type(1) .rail-label', - flightLabelSelector: '#ex1 div.aria-widget-slider:nth-of-type(2) .rail-label', + hotelLabelSelector: + '#ex1 .slider-multithumb:nth-of-type(1) [role=slider] .value', + flightLabelSelector: + '#ex1 .slider-multithumb:nth-of-type(2) [role=slider] .value', }; const verifyAllValues = async function ( @@ -54,7 +55,7 @@ ariaTest( exampleFile, 'slider-role', async (t) => { - await assertAriaRoles(t, 'ex1', 'slider', '4', 'img'); + await assertAriaRoles(t, 'ex1', 'slider', '4', 'g'); } );