From d6ed9a9aa6cf3c90e0706b6a0e9e32b790240f6b Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Mon, 19 Apr 2021 20:54:51 -0500 Subject: [PATCH 01/25] initial slider ranking code --- examples/index.html | 7 + examples/slider/css/slider-rating.css | 120 +++++++++ examples/slider/js/slider-rating.js | 374 ++++++++++++++++++++++++++ examples/slider/slider-rating.html | 283 +++++++++++++++++++ 4 files changed, 784 insertions(+) create mode 100644 examples/slider/css/slider-rating.css create mode 100644 examples/slider/js/slider-rating.js create mode 100644 examples/slider/slider-rating.html diff --git a/examples/index.html b/examples/index.html index 13eb4991ed..3db492ee14 100644 --- a/examples/index.html +++ b/examples/index.html @@ -147,6 +147,7 @@

Examples by Role

  • Checkbox (Two State)
  • Editor Menubar
  • Color Viewer Slider
  • +
  • Rating Slider
  • Date Picker Spin Button
  • Navigation Treeview
  • @@ -305,6 +306,7 @@

    Examples by Role

  • Horizontal Multi-Thumb Slider
  • Slider with aria-orientation and aria-valuetext
  • Color Viewer Slider
  • +
  • Rating Slider
  • @@ -588,6 +590,7 @@

    Examples By Properties and States

  • Radio Group Using aria-activedescendant
  • Radio Group Using Roving tabindex
  • Color Viewer Slider
  • +
  • Rating Slider
  • Date Picker Spin Button
  • Table
  • Tabs with Automatic Activation
  • @@ -625,6 +628,7 @@

    Examples By Properties and States

  • Radio Group Using aria-activedescendant
  • Radio Group Using Roving tabindex
  • Color Viewer Slider
  • +
  • Rating Slider
  • Date Picker Spin Button
  • Tabs with Automatic Activation
  • Tabs with Manual Activation
  • @@ -769,6 +773,7 @@

    Examples By Properties and States

  • Horizontal Multi-Thumb Slider
  • Slider with aria-orientation and aria-valuetext
  • Color Viewer Slider
  • +
  • Rating Slider
  • Date Picker Spin Button
  • @@ -781,6 +786,7 @@

    Examples By Properties and States

  • Horizontal Multi-Thumb Slider
  • Slider with aria-orientation and aria-valuetext
  • Color Viewer Slider
  • +
  • Rating Slider
  • Date Picker Spin Button
  • Toolbar
  • @@ -794,6 +800,7 @@

    Examples By Properties and States

  • Horizontal Multi-Thumb Slider
  • Slider with aria-orientation and aria-valuetext
  • Color Viewer Slider
  • +
  • Rating Slider
  • Date Picker Spin Button
  • Toolbar
  • diff --git a/examples/slider/css/slider-rating.css b/examples/slider/css/slider-rating.css new file mode 100644 index 0000000000..a6c7ca6d76 --- /dev/null +++ b/examples/slider/css/slider-rating.css @@ -0,0 +1,120 @@ +/* CSS Document */ + +.color-viewer-sliders label { + display: block; +} + +.color-viewer-sliders .color-box { + width: 200px; + height: 200px; + border: black solid medium; + text-align: center; + padding: 0.25em; + forced-color-adjust: none; +} + +.color-viewer-sliders .color-info { + padding-top: 5px; +} + +.color-viewer-sliders .color-info label { + margin-top: 0.25em; + display: block; +} + +.color-slider { + margin: 0; + margin-bottom: 1em; + padding: 2px; + touch-action: pan-y; + width: 350px; + outline: none; +} + +.color-slider div, +.color-slider svg { + display: block; + width: 350px; +} + +.color-slider-label { + margin: 0; + padding: 0; + font-weight: bold; +} + +.color-slider .value { + color: currentColor; + fill: currentColor; +} + +.color-slider .valueBackground { + fill: white; + stroke-width: 0; +} + +.color-slider .rail { + fill: #bbb; + stroke: currentColor; + stroke-width: 2px; + opacity: 0.8; +} + +.color-slider .fill { + stroke-width: 0; + opacity: 0.5; + pointer-events: none; +} + +.color-slider.red .fill { + fill: red; +} + +.color-slider.green .fill { + fill: green; +} + +.color-slider.blue .fill { + fill: blue; +} + +.color-slider .thumb { + fill: #666; + stroke: currentColor; + stroke-width: 2px; +} + +.color-slider .focus { + fill: transparent; + stroke: transparent; + stroke-width: 2px; +} + +.color-slider:focus .value, +.color-slider:hover .value { + font-weight: bold; +} + +.color-slider:focus .fill, +.color-slider:hover .fill { + opacity: 1; +} + +.color-slider:focus .rail, +.color-slider:hover .rail { + fill: transparent; + stroke: currentColor; + opacity: 1; +} + +.color-slider:focus .thumb, +.color-slider:hover .thumb { + fill: #ddd; + stroke: currentColor; + opacity: 1; +} + +.color-slider:focus .focus, +.color-slider:hover .focus { + stroke: currentColor; +} diff --git a/examples/slider/js/slider-rating.js b/examples/slider/js/slider-rating.js new file mode 100644 index 0000000000..0d601ca33e --- /dev/null +++ b/examples/slider/js/slider-rating.js @@ -0,0 +1,374 @@ +'use strict'; +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: slider-color-viewer.js + * + * Desc: ColorViewerSliders widget that implements ARIA Authoring Practices + */ + +// Create ColorViewerSliders that contains value, valuemin, valuemax, and valuenow +class ColorViewerSliders { + constructor(domNode) { + this.domNode = domNode; + + this.pointerSlider = false; + + this.sliders = {}; + + this.svgWidth = 310; + this.svgHeight = 50; + this.borderWidth = 2; + + this.valueY = 20; + + this.railX = 15; + this.railY = 26; + this.railWidth = 275; + this.railHeight = 14; + + this.thumbHeight = this.railHeight; + this.thumbWidth = this.thumbHeight; + this.rectRadius = this.railHeight / 4; + + this.focusY = this.borderWidth; + this.focusWidth = 36; + this.focusHeight = 48; + + this.initSliderRefs(this.sliders, 'red'); + this.initSliderRefs(this.sliders, 'green'); + this.initSliderRefs(this.sliders, 'blue'); + + document.body.addEventListener( + 'pointerup', + this.onThumbPointerUp.bind(this) + ); + + this.colorBoxNode = domNode.querySelector('.color-box'); + this.colorValueHexNode = domNode.querySelector('input.color-value-hex'); + this.colorValueRGBNode = domNode.querySelector('input.color-value-rgb'); + } + + initSliderRefs(sliderRef, color) { + sliderRef[color] = {}; + var n = this.domNode.querySelector('.color-slider.' + color); + sliderRef[color].sliderNode = n; + + sliderRef[color].svgNode = n.querySelector('svg'); + sliderRef[color].svgNode.setAttribute('width', this.svgWidth); + sliderRef[color].svgNode.setAttribute('height', this.svgHeight); + sliderRef[color].svgPoint = sliderRef[color].svgNode.createSVGPoint(); + + sliderRef[color].valueNode = n.querySelector('.value'); + sliderRef[color].valueNode.setAttribute('y', this.valueY); + + sliderRef[color].thumbNode = n.querySelector('.thumb'); + sliderRef[color].thumbNode.setAttribute('width', this.thumbWidth); + sliderRef[color].thumbNode.setAttribute('height', this.thumbHeight); + sliderRef[color].thumbNode.setAttribute('y', this.railY); + sliderRef[color].thumbNode.setAttribute('rx', this.rectRadius); + + sliderRef[color].focusNode = n.querySelector('.focus'); + sliderRef[color].focusNode.setAttribute( + 'width', + this.focusWidth - this.borderWidth + ); + sliderRef[color].focusNode.setAttribute( + 'height', + this.focusHeight - this.borderWidth + ); + sliderRef[color].focusNode.setAttribute('y', this.focusY); + sliderRef[color].focusNode.setAttribute('rx', this.rectRadius); + + sliderRef[color].railNode = n.querySelector('.color-slider .rail'); + sliderRef[color].railNode.setAttribute('x', this.railX); + sliderRef[color].railNode.setAttribute('y', this.railY); + sliderRef[color].railNode.setAttribute('width', this.railWidth); + sliderRef[color].railNode.setAttribute('height', this.railHeight); + sliderRef[color].railNode.setAttribute('rx', this.rectRadius); + + sliderRef[color].fillNode = n.querySelector('.color-slider .fill'); + sliderRef[color].fillNode.setAttribute('x', this.railX); + sliderRef[color].fillNode.setAttribute('y', this.railY); + sliderRef[color].fillNode.setAttribute('width', this.railWidth); + sliderRef[color].fillNode.setAttribute('height', this.railHeight); + sliderRef[color].fillNode.setAttribute('rx', this.rectRadius); + } + + // Initialize slider + init() { + for (var slider in this.sliders) { + if (this.sliders[slider].sliderNode.tabIndex != 0) { + this.sliders[slider].sliderNode.tabIndex = 0; + } + + this.sliders[slider].railNode.addEventListener( + 'click', + this.onRailClick.bind(this) + ); + + this.sliders[slider].sliderNode.addEventListener( + 'keydown', + this.onSliderKeyDown.bind(this) + ); + + this.sliders[slider].sliderNode.addEventListener( + 'pointerdown', + this.onThumbPointerDown.bind(this) + ); + + this.sliders[slider].valueNode.addEventListener( + 'keydown', + this.onSliderKeyDown.bind(this) + ); + + this.sliders[slider].valueNode.addEventListener( + 'pointerdown', + this.onThumbPointerDown.bind(this) + ); + + this.sliders[slider].sliderNode.addEventListener( + 'pointermove', + this.onThumbPointerMove.bind(this) + ); + + this.moveSliderTo( + this.sliders[slider], + this.getValueNow(this.sliders[slider]) + ); + } + } + + // Get point in global SVG space + getSVGPoint(slider, event) { + slider.svgPoint.x = event.clientX; + slider.svgPoint.y = event.clientY; + return slider.svgPoint.matrixTransform( + slider.svgNode.getScreenCTM().inverse() + ); + } + + getSlider(domNode) { + if (!domNode.classList.contains('color-slider')) { + if (domNode.tagName.toLowerCase() === 'rect') { + domNode = domNode.parentNode.parentNode; + } else { + domNode = domNode.parentNode.querySelector('.color-slider'); + } + } + + if (this.sliders.red.sliderNode === domNode) { + return this.sliders.red; + } + + if (this.sliders.green.sliderNode === domNode) { + return this.sliders.green; + } + + return this.sliders.blue; + } + + getValueMin(slider) { + return parseInt(slider.sliderNode.getAttribute('aria-valuemin')); + } + + getValueNow(slider) { + return parseInt(slider.sliderNode.getAttribute('aria-valuenow')); + } + + getValueMax(slider) { + return parseInt(slider.sliderNode.getAttribute('aria-valuemax')); + } + + moveSliderTo(slider, value) { + var pos, offsetX, valueWidth; + var valueMin = this.getValueMin(slider); + var valueNow = this.getValueNow(slider); + var valueMax = this.getValueMax(slider); + + value = Math.min(Math.max(value, valueMin), valueMax); + + valueNow = value; + slider.sliderNode.setAttribute('aria-valuenow', value); + + offsetX = Math.round( + (valueNow * (this.railWidth - this.thumbWidth)) / (valueMax - valueMin) + ); + + pos = this.railX + offsetX; + + slider.thumbNode.setAttribute('x', pos); + slider.fillNode.setAttribute('width', offsetX + this.rectRadius); + + slider.valueNode.textContent = valueNow; + valueWidth = slider.valueNode.getBBox().width; + + pos = this.railX + offsetX - (valueWidth - this.thumbWidth) / 2; + slider.valueNode.setAttribute('x', pos); + + pos = this.railX + offsetX - (this.focusWidth - this.thumbWidth) / 2; + slider.focusNode.setAttribute('x', pos); + + this.updateColorBox(); + } + + onSliderKeyDown(event) { + var flag = false; + + var slider = this.getSlider(event.currentTarget); + + var valueMin = this.getValueMin(slider); + var valueNow = this.getValueNow(slider); + var valueMax = this.getValueMax(slider); + + switch (event.key) { + case 'Left': + case 'ArrowLeft': + case 'Down': + case 'ArrowDown': + this.moveSliderTo(slider, valueNow - 1); + flag = true; + break; + + case 'Right': + case 'ArrowRight': + case 'Up': + case 'ArrowUp': + this.moveSliderTo(slider, valueNow + 1); + flag = true; + break; + + case 'PageDown': + this.moveSliderTo(slider, valueNow - 10); + flag = true; + break; + + case 'PageUp': + this.moveSliderTo(slider, valueNow + 10); + flag = true; + break; + + case 'Home': + this.moveSliderTo(slider, valueMin); + flag = true; + break; + + case 'End': + this.moveSliderTo(slider, valueMax); + flag = true; + break; + + default: + break; + } + + if (flag) { + event.preventDefault(); + event.stopPropagation(); + } + } + + onThumbPointerDown(event) { + this.pointerSlider = this.getSlider(event.currentTarget); + + // Set focus to the clicked on + this.pointerSlider.sliderNode.focus(); + + event.preventDefault(); + event.stopPropagation(); + } + + onThumbPointerUp() { + this.pointerSlider = false; + } + + onThumbPointerMove(event) { + if ( + this.pointerSlider && + this.pointerSlider.sliderNode.contains(event.target) + ) { + let x = this.getSVGPoint(this.pointerSlider, event).x; + let min = this.getValueMin(this.pointerSlider); + let max = this.getValueMax(this.pointerSlider); + let diffX = x - this.railX; + let value = Math.round((diffX * (max - min)) / this.railWidth); + this.moveSliderTo(this.pointerSlider, value); + + event.preventDefault(); + event.stopPropagation(); + } + } + + // handle click event on the rail + onRailClick(event) { + var slider = this.getSlider(event.currentTarget); + + var x = this.getSVGPoint(slider, event).x; + var min = this.getValueMin(slider); + var max = this.getValueMax(slider); + var diffX = x - this.railX; + var value = Math.round((diffX * (max - min)) / this.railWidth); + this.moveSliderTo(slider, value); + + event.preventDefault(); + event.stopPropagation(); + + // Set focus to the clicked handle + slider.sliderNode.focus(); + } + + getColorHex() { + var r = parseInt( + this.sliders.red.sliderNode.getAttribute('aria-valuenow') + ).toString(16); + var g = parseInt( + this.sliders.green.sliderNode.getAttribute('aria-valuenow') + ).toString(16); + var b = parseInt( + this.sliders.blue.sliderNode.getAttribute('aria-valuenow') + ).toString(16); + + if (r.length === 1) { + r = '0' + r; + } + if (g.length === 1) { + g = '0' + g; + } + if (b.length === 1) { + b = '0' + b; + } + + return '#' + r + g + b; + } + + getColorRGB() { + var r = this.sliders.red.sliderNode.getAttribute('aria-valuenow'); + var g = this.sliders.green.sliderNode.getAttribute('aria-valuenow'); + var b = this.sliders.blue.sliderNode.getAttribute('aria-valuenow'); + + return r + ', ' + g + ', ' + b; + } + + updateColorBox() { + if (this.colorBoxNode) { + this.colorBoxNode.style.backgroundColor = this.getColorHex(); + } + + if (this.colorValueHexNode) { + this.colorValueHexNode.value = this.getColorHex(); + } + + if (this.colorValueRGBNode) { + this.colorValueRGBNode.value = this.getColorRGB(); + } + } +} +// Initialize ColorViewerSliders on the page +window.addEventListener('load', function () { + var cps = document.querySelectorAll('.color-viewer-sliders'); + for (let i = 0; i < cps.length; i++) { + let s = new ColorViewerSliders(cps[i]); + s.init(); + } +}); diff --git a/examples/slider/slider-rating.html b/examples/slider/slider-rating.html new file mode 100644 index 0000000000..9ccaf49a1b --- /dev/null +++ b/examples/slider/slider-rating.html @@ -0,0 +1,283 @@ + + + + + + Rating Slider Example | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + + + +
    +

    Rating Slider Example

    +
    +

    + WARNING! Some users of touch-based assistive technologies may experience difficulty utilizing widgets that implement this slider pattern because the gestures their assistive technology provides for operating sliders may not yet generate the necessary output. + To change the slider value, touch-based assistive technologies need to respond to user gestures for incrementing and decrementing the value by synthesizing key events. + This is a new convention that may not be fully implemented by some assistive technologies. + Authors should fully test slider widgets using assistive technologies on devices where touch is a primary input mechanism before considering incorporation into production systems. +

    +
    +

    + Following is an example of a rating input that demonstrates the + slider design pattern. + Rating level is indicated by the number of full and half stars selected by the user. +

    +

    Similar examples include:

    + +
    +
    +

    Example

    +
    + +
    +

    Rating

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    +

    Accessibility Features

    +
      +
    • To highlight the interactive nature of the thumb, the focus ring is drawn around the thumb and the value.
    • +
    • + To ensure the borders of the slider rail, thumb and focus ring have sufficient contrast with the background when high contrast settings invert colors, the CSS currentColor value for the stroke property is used for the SVG rect elements to synchronize the border color with text content. + If specific colors were used to specify the stroke property, the color of these elements would remain the same in high contrast mode, which could lead to insufficient contrast between them and their background or even make them invisible if their color were to match the high contrast mode background. + NOTE: The SVG elements need to be inline to use currentColor. +
    • +
    +
    + +
    +

    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
    groupdiv +
      +
    • Identifies the div as a group.
    • +
    • The group and its accessible name inform assistive technology users that the three sliders are related to one another and serve the purpose of choosing a color.
    • +
    +
    aria-labelledby="IDREF"divRefers to the heading element that provides the accessible name for the group.
    + slider + + div + +
      +
    • Identifies the element as a slider.
    • +
    • Set on the div that represents the movable thumb because it is the operable element that controls the slider value.
    • +
    +
    + tabindex="0" + + div + Includes the slider thumb in the page tab sequence.
    + aria-valuemax="255" + + div + Specifies the maximum value of the slider.
    + aria-valuemin="0" + + div + Specifies the minimum value of the slider.
    + aria-valuenow="NUMBER" + + div + Indicates the current value of the slider.
    + aria-labelledby="IDREF" + + div + Refers to the element containing the name of the slider.
    aria-hidden="true"svgRemoves the SVG elements from the accessibility tree. Some assistive technologies will describe the SVG elements, unless they are explicitly hidden.
    +
    + +
    +

    Javascript and CSS Source Code

    + +
    + +
    +

    HTML Source Code

    + +
    + + +
    + +
    + + + From 9912f80b8adec8a84eb380a70cfbf818e97a91eb Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Wed, 21 Apr 2021 15:54:59 -0500 Subject: [PATCH 02/25] update the rating example --- examples/index.html | 9 - examples/slider/css/slider-rating.css | 169 +++++------ examples/slider/css/text-slider.css | 48 --- examples/slider/css/vertical-slider.css | 51 ---- examples/slider/js/slider-rating.js | 385 +++++++----------------- examples/slider/js/text-slider.js | 210 ------------- examples/slider/js/vertical-slider.js | 221 -------------- examples/slider/slider-2.html | 286 ------------------ examples/slider/slider-rating.html | 74 +++-- 9 files changed, 248 insertions(+), 1205 deletions(-) delete mode 100644 examples/slider/css/text-slider.css delete mode 100644 examples/slider/css/vertical-slider.css delete mode 100644 examples/slider/js/text-slider.js delete mode 100644 examples/slider/js/vertical-slider.js delete mode 100644 examples/slider/slider-2.html diff --git a/examples/index.html b/examples/index.html index 3db492ee14..d4e1a89743 100644 --- a/examples/index.html +++ b/examples/index.html @@ -304,7 +304,6 @@

    Examples by Role

    @@ -680,10 +679,6 @@

    Examples By Properties and States

    aria-multiselectable Listboxes with Rearrangeable Options - - aria-orientation - Slider with aria-orientation and aria-valuetext - aria-owns Navigation Treeview @@ -771,7 +766,6 @@

    Examples By Properties and States