From 4e3d0e77fa2fc903bc3f4136707db36f4729d18d Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Wed, 23 Dec 2020 09:11:08 -0600 Subject: [PATCH 1/5] updated multithumb slider to use SVG and support mobile devices --- examples/index.html | 16 +- examples/slider/css/multithumb-slider.css | 48 - examples/slider/css/slider-multithumb.css | 94 ++ examples/slider/js/multithumb-slider.js | 235 ---- examples/slider/js/slider-multithumb.js | 380 ++++++ examples/slider/multithumb-slider.html | 277 ---- examples/slider/slider-multithumb-mobile.html | 361 +++++ examples/slider/slider-multithumb.html | 295 +++++ test/tests/slider_slider-multithumb-mobile.js | 1163 +++++++++++++++++ ...tithumb.js => slider_slider-multithumb.js} | 15 +- 10 files changed, 2312 insertions(+), 572 deletions(-) delete mode 100644 examples/slider/css/multithumb-slider.css create mode 100644 examples/slider/css/slider-multithumb.css delete mode 100644 examples/slider/js/multithumb-slider.js create mode 100644 examples/slider/js/slider-multithumb.js delete mode 100644 examples/slider/multithumb-slider.html create mode 100644 examples/slider/slider-multithumb-mobile.html create mode 100644 examples/slider/slider-multithumb.html create mode 100644 test/tests/slider_slider-multithumb-mobile.js rename test/tests/{slider_multithumb.js => slider_slider-multithumb.js} (98%) 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..e14fedb2cf --- /dev/null +++ b/examples/slider/css/slider-multithumb.css @@ -0,0 +1,94 @@ +/* CSS Document */ + +.slider-multithumb { + padding: 6px; + width: 550px; +} + +.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; +} + +.slider-multithumb.mobile .input-minimum, +.slider-multithumb.mobile .input-maximum { + position: relative; + top: 20px; + 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; +} + +.slider-multithumb .slider-group .rail { + stroke: transparent; + stroke-width: 1px; + fill: #ccc; +} + +.slider-multithumb .slider-group .thumb { + stroke-width: 0; + fill: #444; +} + +.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: #adddff; + fill: #adddff; +} + +.slider-multithumb [role="slider"]:focus { + outline: none; +} + +.slider-multithumb [role="slider"]:focus .focus { + stroke: currentColor; +} + +.slider-multithumb [role="slider"]:focus .thumb { + fill: currentColor; +} + +.slider-multithumb [role="slider"]:focus .value { + 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..0df3108693 --- /dev/null +++ b/examples/slider/js/slider-multithumb.js @@ -0,0 +1,380 @@ +/* + * 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.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.valueTop = 12; + + this.railHeight = 6; + this.railWidth = 300; + this.railTop = 30; + this.railLeft = 10; + + this.thumbWidth = 20; + this.thumb2Width = 2 * this.thumbWidth; + this.thumbHeight = 20; + this.thumbTop = 23; + + this.focusRadius = 16; + this.focusOffset = 8; + + this.thumbMiddle = this.thumbTop + this.thumbHeight / 2; + this.thumbBottom = this.thumbTop + this.thumbHeight; + this.minSliderValueNode.setAttribute('y', this.valueTop); + this.maxSliderValueNode.setAttribute('y', this.valueTop); + + this.minSliderFocusNode.setAttribute('r', this.focusRadius); + this.minSliderFocusNode.setAttribute('cy', this.thumbMiddle); + this.maxSliderFocusNode.setAttribute('r', this.focusRadius); + this.maxSliderFocusNode.setAttribute('cy', this.thumbMiddle); + + this.railNode.setAttribute('y', this.railTop); + this.railNode.setAttribute('x', this.railLeft); + this.railNode.setAttribute('height', this.railHeight); + this.railNode.setAttribute('width', this.railWidth + this.thumbWidth); + + 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, + cx, + 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.railLeft; + 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 + cx = `${pos + this.focusOffset}`; + this.minSliderFocusNode.setAttribute('cx', cx); + + 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 + cx = `${pos + 2 * this.thumbWidth - this.focusOffset + 1}`; + this.maxSliderFocusNode.setAttribute('cx', cx); + + 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 - self.railNode.getBoundingClientRect().left; + + if (isMinSlider) { + diffX -= self.thumbWidth / 2; + } else { + diffX -= (3 * self.thumbWidth) / 2; + } + + var value = parseInt( + ((self.sliderMaxValue - self.sliderMinValue) * diffX) / self.railWidth + ); + + self.moveSliderTo(sliderNode, value); + + e.preventDefault(); + e.stopPropagation(); + }; + + var onMouseup = function () { + document.removeEventListener('mousemove', onMousemove); + document.removeEventListener('mouseup', onMouseup); + }; + + var sliderNode = event.currentTarget; + var isMinSlider = this.isMinSlider(sliderNode); + var self = this; + + // 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..301c79b7fb --- /dev/null +++ b/examples/slider/slider-multithumb-mobile.html @@ -0,0 +1,361 @@ + + + + + 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

    +
      +
    • A border is placed around the entire multi-thumbslider when one of the elements within the slider receives focus.
    • +
    • The placement of the slider value above the thumb supports users with low vision by allow them to more easily view the value of the slider as they move the thumb.
    • +
    • A circle SVG image is used for indicating the slider with keyboard focus.
    • +
    • The use SVG images enables the thumb, focus ring and rail of the slider to adapt to operating system high contrast colors.
    • +
    • The CSS currentColor value for the fill and stroke properties of the thumb and slider is used to support the override colors in high contrast mode.
    • +
    • input[type=number] for setting the low and higher values of the range support people using mobile devices to set the values.
    • +
    • input[type=number] have tabindex=-1 so they are not part of the tab sequence of the page for keyboard users.
    • +
    +
    + +
    +

    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..ddd5a42961 --- /dev/null +++ b/examples/slider/slider-multithumb.html @@ -0,0 +1,295 @@ + + + + + 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

    +
      +
    • A border is placed around the entire multi-thumbslider when one of the elements within the slider receives focus.
    • +
    • The placement of the slider value above the thumb supports users with low vision by allow them to more easily view the value of the slider as they move the thumb.
    • +
    • A circle SVG image is used for indicating the slider with keyboard focus.
    • +
    • The use SVG images enables the thumb, focus ring and rail of the slider to adapt to operating system high contrast colors.
    • +
    • The CSS currentColor value for the fill and stroke properties of the thumb and slider is used to support the override colors in high contrast mode.
    • +
    +
    + +
    +

    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'); } ); From 93804ea37a4bc8b021b9b7a9cfd871498c4648a0 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Wed, 30 Dec 2020 19:29:35 -0600 Subject: [PATCH 2/5] updated keyboard focus styling --- examples/slider/css/slider-multithumb.css | 2 +- examples/slider/js/slider-multithumb.js | 58 ++++++++++++------- examples/slider/slider-multithumb-mobile.html | 42 ++++++-------- examples/slider/slider-multithumb.html | 46 ++++++--------- 4 files changed, 73 insertions(+), 75 deletions(-) diff --git a/examples/slider/css/slider-multithumb.css b/examples/slider/css/slider-multithumb.css index e14fedb2cf..4a62a90341 100644 --- a/examples/slider/css/slider-multithumb.css +++ b/examples/slider/css/slider-multithumb.css @@ -21,7 +21,7 @@ .slider-multithumb.mobile .input-minimum, .slider-multithumb.mobile .input-maximum { position: relative; - top: 20px; + top: 32px; height: 1.5em; } diff --git a/examples/slider/js/slider-multithumb.js b/examples/slider/js/slider-multithumb.js index 0df3108693..77fe8eecd6 100644 --- a/examples/slider/js/slider-multithumb.js +++ b/examples/slider/js/slider-multithumb.js @@ -11,6 +11,7 @@ class SliderMultithumb { constructor(domNode) { this.domNode = domNode; + this.svgNode = domNode.querySelector('svg'); this.railNode = domNode.querySelector('.rail'); this.minSliderNode = domNode.querySelector('[role=slider].minimum'); @@ -56,33 +57,46 @@ class SliderMultithumb { // Dimensions of the slider focus ring, thumb and rail - this.valueTop = 12; + this.svgWidth = 360; + this.svgHeight = 80; + + this.valueTop = 24; + this.valueHeight = this.minSliderValueNode.getBoundingClientRect().height; this.railHeight = 6; this.railWidth = 300; - this.railTop = 30; - this.railLeft = 10; + this.railY = 42; + this.railX = 10; - this.thumbWidth = 20; + this.thumbTop = 30; + this.thumbHeight = 30; + this.thumbWidth = 30; this.thumb2Width = 2 * this.thumbWidth; - this.thumbHeight = 20; - this.thumbTop = 23; + this.thumbMiddle = this.thumbTop + this.thumbHeight / 2; + this.thumbBottom = this.thumbTop + this.thumbHeight; - this.focusRadius = 16; 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.thumbMiddle = this.thumbTop + this.thumbHeight / 2; - this.thumbBottom = this.thumbTop + this.thumbHeight; this.minSliderValueNode.setAttribute('y', this.valueTop); this.maxSliderValueNode.setAttribute('y', this.valueTop); - this.minSliderFocusNode.setAttribute('r', this.focusRadius); - this.minSliderFocusNode.setAttribute('cy', this.thumbMiddle); - this.maxSliderFocusNode.setAttribute('r', this.focusRadius); - this.maxSliderFocusNode.setAttribute('cy', this.thumbMiddle); - - this.railNode.setAttribute('y', this.railTop); - this.railNode.setAttribute('x', this.railLeft); + 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); @@ -155,7 +169,7 @@ class SliderMultithumb { var valueMax, valueMin, pos, - cx, + x, points = '', width; @@ -179,7 +193,7 @@ class SliderMultithumb { sliderNode.setAttribute('aria-valuenow', value); sliderNode.setAttribute('aria-valuetext', dollarValue); - pos = this.railLeft; + pos = this.railX; pos += Math.round( (value * (this.railWidth - this.thumbWidth)) / (this.sliderMaxValue - this.sliderMinValue) @@ -196,8 +210,8 @@ class SliderMultithumb { } // move the SVG focus ring and thumb elements - cx = `${pos + this.focusOffset}`; - this.minSliderFocusNode.setAttribute('cx', cx); + x = pos - this.focusOffset - 1; + this.minSliderFocusNode.setAttribute('x', x); points = `${pos},${this.thumbTop}`; points += ` ${pos + this.thumbWidth},${this.thumbMiddle}`; @@ -214,8 +228,8 @@ class SliderMultithumb { this.minSliderRight = pos; } else { // move the SVG focus ring and thumb elements - cx = `${pos + 2 * this.thumbWidth - this.focusOffset + 1}`; - this.maxSliderFocusNode.setAttribute('cx', cx); + x = pos + this.thumbWidth - this.focusOffset + 1; + this.maxSliderFocusNode.setAttribute('x', x); points = `${pos + this.thumbWidth},${this.thumbMiddle}`; points += ` ${pos + this.thumb2Width},${this.thumbTop}`; diff --git a/examples/slider/slider-multithumb-mobile.html b/examples/slider/slider-multithumb-mobile.html index 301c79b7fb..3f12b51401 100644 --- a/examples/slider/slider-multithumb-mobile.html +++ b/examples/slider/slider-multithumb-mobile.html @@ -65,7 +65,7 @@

    Hotel Price Range

    /> - + Hotel Price Range aria-valuetext="$100" aria-valuemax="250" aria-label="Hotel Minimum Price"> - 0 - - + 0 + + Hotel Price Range aria-valuetext="$250" aria-valuemax="400" aria-label="Hotel Maximum Price"> - 0 - - + 0 + + @@ -118,8 +114,8 @@

    Flight Price Range

    value="0" aria-label="Flight Minimum Price"/>
    - - + + Flight Price Range aria-valuetext="$100" aria-valuemax="250" aria-label="Flight Minimum Price"> - 0 - - + 0 + + Flight Price Range aria-valuetext="$250" aria-valuemax="1000" aria-label="Flight Maximum Price"> - 0 - - + 0 + + @@ -168,8 +160,8 @@

    Accessibility Features

    • A border is placed around the entire multi-thumbslider when one of the elements within the slider receives focus.
    • The placement of the slider value above the thumb supports users with low vision by allow them to more easily view the value of the slider as they move the thumb.
    • -
    • A circle SVG image is used for indicating the slider with keyboard focus.
    • -
    • The use SVG images enables the thumb, focus ring and rail of the slider to adapt to operating system high contrast colors.
    • +
    • The SVG rect element is used to encapsulate both the slider and slider value for the slider with keyboard focus.
    • +
    • The use SVG polygon, rect and text elements enables the thumb, focus ring, value and rail of the slider to adapt to operating system high contrast colors.
    • The CSS currentColor value for the fill and stroke properties of the thumb and slider is used to support the override colors in high contrast mode.
    • input[type=number] for setting the low and higher values of the range support people using mobile devices to set the values.
    • input[type=number] have tabindex=-1 so they are not part of the tab sequence of the page for keyboard users.
    • diff --git a/examples/slider/slider-multithumb.html b/examples/slider/slider-multithumb.html index ddd5a42961..6697b4fd73 100644 --- a/examples/slider/slider-multithumb.html +++ b/examples/slider/slider-multithumb.html @@ -55,7 +55,7 @@

      Example

      Hotel Price Range

      - + Hotel Price Range aria-valuetext="$100" aria-valuemax="250" aria-label="Hotel Minimum Price"> - 0 - - + 0 + + Hotel Price Range aria-valuetext="$250" aria-valuemax="400" aria-label="Hotel Maximum Price"> - 0 - - + 0 + + -
      +

      Flight Price Range

      - - + + Flight Price Range aria-valuetext="$100" aria-valuemax="250" aria-label="Flight Minimum Price"> - 0 - - + 0 + + Flight Price Range aria-valuetext="$250" aria-valuemax="1000" aria-label="Flight Maximum Price"> - 0 - - + 0 + + -
      + @@ -129,8 +121,8 @@

      Accessibility Features

      • A border is placed around the entire multi-thumbslider when one of the elements within the slider receives focus.
      • The placement of the slider value above the thumb supports users with low vision by allow them to more easily view the value of the slider as they move the thumb.
      • -
      • A circle SVG image is used for indicating the slider with keyboard focus.
      • -
      • The use SVG images enables the thumb, focus ring and rail of the slider to adapt to operating system high contrast colors.
      • +
      • The SVG rect element is used to encapsulate both the slider and slider value for the slider with keyboard focus.
      • +
      • The use SVG polygon, rect and text elements enables the thumb, focus ring, value and rail of the slider to adapt to operating system high contrast colors.
      • The CSS currentColor value for the fill and stroke properties of the thumb and slider is used to support the override colors in high contrast mode.
      From d13c0429b5282478ecd51c66dd504a009f774cce Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Wed, 30 Dec 2020 19:35:54 -0600 Subject: [PATCH 3/5] fixed linting issues --- examples/slider/slider-multithumb-mobile.html | 21 +++++++++--------- examples/slider/slider-multithumb.html | 22 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/examples/slider/slider-multithumb-mobile.html b/examples/slider/slider-multithumb-mobile.html index 3f12b51401..68efa03923 100644 --- a/examples/slider/slider-multithumb-mobile.html +++ b/examples/slider/slider-multithumb-mobile.html @@ -64,8 +64,9 @@

      Hotel Price Range

      aria-label="Hotel Minimum Price" /> + - + Hotel Price Range aria-valuemax="250" aria-label="Hotel Minimum Price"> 0 - - + + Hotel Price Range aria-valuemax="400" aria-label="Hotel Maximum Price"> 0 - - + + @@ -115,7 +116,7 @@

      Flight Price Range

      aria-label="Flight Minimum Price"/>
      - + Flight Price Range aria-valuemax="250" aria-label="Flight Minimum Price"> 0 - - + + Flight Price Range aria-valuemax="1000" aria-label="Flight Maximum Price"> 0 - - + + diff --git a/examples/slider/slider-multithumb.html b/examples/slider/slider-multithumb.html index 6697b4fd73..8ce1e57ba0 100644 --- a/examples/slider/slider-multithumb.html +++ b/examples/slider/slider-multithumb.html @@ -55,7 +55,7 @@

      Example

      Hotel Price Range

      - + Hotel Price Range aria-valuemax="250" aria-label="Hotel Minimum Price"> 0 - - - + + + Hotel Price Range aria-valuemax="400" aria-label="Hotel Maximum Price"> 0 - - + +

      Flight Price Range

      - + Flight Price Range aria-valuemax="250" aria-label="Flight Minimum Price"> 0 - - + + Flight Price Range aria-valuemax="1000" aria-label="Flight Maximum Price"> 0 - - + +
      From 9796548e959f1ce97a84800d437b1af2d6ba2c92 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Fri, 8 Jan 2021 15:59:27 -0600 Subject: [PATCH 4/5] improved mousemove code --- examples/slider/css/slider-multithumb.css | 4 +++- examples/slider/js/slider-multithumb.js | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/slider/css/slider-multithumb.css b/examples/slider/css/slider-multithumb.css index 4a62a90341..d4f0668bbb 100644 --- a/examples/slider/css/slider-multithumb.css +++ b/examples/slider/css/slider-multithumb.css @@ -2,7 +2,7 @@ .slider-multithumb { padding: 6px; - width: 550px; + width: 350px; } .slider-multithumb.focus { @@ -16,6 +16,7 @@ display: grid; grid-template-columns: 100px 340px 100px; grid-column-gap: 10px; + width: 550px; } .slider-multithumb.mobile .input-minimum, @@ -90,5 +91,6 @@ } .slider-multithumb [role="slider"]:focus .value { + color: currentColor; font-weight: bold; } diff --git a/examples/slider/js/slider-multithumb.js b/examples/slider/js/slider-multithumb.js index 77fe8eecd6..bcc131f6ea 100644 --- a/examples/slider/js/slider-multithumb.js +++ b/examples/slider/js/slider-multithumb.js @@ -315,23 +315,23 @@ class SliderMultithumb { onSliderMousedown(event) { var onMousemove = function (e) { - var diffX = e.pageX - self.railNode.getBoundingClientRect().left; + var diffX = e.pageX - this.railNode.getBoundingClientRect().left; if (isMinSlider) { - diffX -= self.thumbWidth / 2; + diffX -= this.thumbWidth / 2; } else { - diffX -= (3 * self.thumbWidth) / 2; + diffX -= (3 * this.thumbWidth) / 2; } var value = parseInt( - ((self.sliderMaxValue - self.sliderMinValue) * diffX) / self.railWidth + ((this.sliderMaxValue - this.sliderMinValue) * diffX) / this.railWidth ); - self.moveSliderTo(sliderNode, value); + this.moveSliderTo(sliderNode, value); e.preventDefault(); e.stopPropagation(); - }; + }.bind(this); var onMouseup = function () { document.removeEventListener('mousemove', onMousemove); @@ -340,7 +340,6 @@ class SliderMultithumb { var sliderNode = event.currentTarget; var isMinSlider = this.isMinSlider(sliderNode); - var self = this; // bind a mousemove event handler to move pointer document.addEventListener('mousemove', onMousemove); From 7b61faf08be7cbce882e647ff856f76f7c97bfc5 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Tue, 26 Jan 2021 09:56:38 -0600 Subject: [PATCH 5/5] updated high contrast styling support --- examples/slider/css/slider-multithumb.css | 14 ++++++++------ examples/slider/js/slider-multithumb.js | 7 ++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/slider/css/slider-multithumb.css b/examples/slider/css/slider-multithumb.css index d4f0668bbb..61d163392b 100644 --- a/examples/slider/css/slider-multithumb.css +++ b/examples/slider/css/slider-multithumb.css @@ -46,17 +46,19 @@ .slider-multithumb .slider-group .value { font-size: 80%; color: currentColor; + fill: currentColor; } .slider-multithumb .slider-group .rail { - stroke: transparent; - stroke-width: 1px; + stroke: currentColor; + stroke-width: 2px; fill: #ccc; } .slider-multithumb .slider-group .thumb { - stroke-width: 0; - fill: #444; + stroke: transparent; + stroke-width: 2px; + fill: currentColor; } .slider-multithumb .slider-group .focus { @@ -74,7 +76,7 @@ } .slider-multithumb.focus .slider-group .rail { - stroke: #adddff; + stroke: currentColor; fill: #adddff; } @@ -87,7 +89,7 @@ } .slider-multithumb [role="slider"]:focus .thumb { - fill: currentColor; + stroke: currentColor; } .slider-multithumb [role="slider"]:focus .value { diff --git a/examples/slider/js/slider-multithumb.js b/examples/slider/js/slider-multithumb.js index bcc131f6ea..9d84c568f0 100644 --- a/examples/slider/js/slider-multithumb.js +++ b/examples/slider/js/slider-multithumb.js @@ -68,9 +68,9 @@ class SliderMultithumb { this.railY = 42; this.railX = 10; - this.thumbTop = 30; - this.thumbHeight = 30; - this.thumbWidth = 30; + 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; @@ -99,6 +99,7 @@ class SliderMultithumb { 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);