diff --git a/.vnurc b/.vnurc
index 460f3625dc..01d8027396 100644
--- a/.vnurc
+++ b/.vnurc
@@ -20,3 +20,6 @@ The “row” role is unnecessary for element “tr”.
Attribute “aria-activedescendant” value should either refer to a descendant element, or should be accompanied by attribute “aria-owns”.
# https://github.com/w3c/aria-practices/issues/1678
Section lacks heading. Consider using “h2”-“h6” elements to add identifying headings to all sections.
+# https://github.com/validator/validator/issues/1096
+Bad value “none” for attribute “role” on element “svg”.
+Bad value “presentation” for attribute “role” on element “svg”.
diff --git a/aria-practices.html b/aria-practices.html
index 083c41a550..72ba1faafb 100644
--- a/aria-practices.html
+++ b/aria-practices.html
@@ -2361,8 +2361,9 @@
Slider
diff --git a/examples/index.html b/examples/index.html
index c3d9417e82..0f478b3984 100644
--- a/examples/index.html
+++ b/examples/index.html
@@ -262,6 +262,7 @@ Examples by Role
@@ -344,6 +345,7 @@ Examples by Role
Horizontal Multi-Thumb Slider
Color Viewer Slider (HC)
Rating Slider (HC)
+ Vertical Temperature Slider (HC)
@@ -603,6 +605,7 @@ Examples By Properties and States
Button (IDL Version)
Color Viewer Slider (HC)
Rating Slider (HC)
+ Vertical Temperature Slider (HC)
Date Picker Spin Button
Toolbar
@@ -660,6 +663,7 @@ Examples By Properties and States
Radio Group Using Roving tabindex (HC)
Color Viewer Slider (HC)
Rating Slider (HC)
+ Vertical Temperature Slider (HC)
Date Picker Spin Button
Tabs with Automatic Activation
Tabs with Manual Activation
@@ -711,6 +715,10 @@ Examples By Properties and States
aria-multiselectable |
Listboxes with Rearrangeable Options |
+
+ aria-orientation |
+ Vertical Temperature Slider |
+
aria-owns |
Navigation Treeview |
@@ -805,6 +813,7 @@ Examples By Properties and States
Horizontal Multi-Thumb Slider
Color Viewer Slider (HC)
Rating Slider (HC)
+ Vertical Temperature Slider (HC)
Date Picker Spin Button
Toolbar
@@ -818,6 +827,7 @@ Examples By Properties and States
Horizontal Multi-Thumb Slider
Color Viewer Slider (HC)
Rating Slider (HC)
+ Vertical Temperature Slider (HC)
Date Picker Spin Button
Toolbar
@@ -831,6 +841,7 @@ Examples By Properties and States
Horizontal Multi-Thumb Slider
Color Viewer Slider (HC)
Rating Slider (HC)
+ Vertical Temperature Slider (HC)
Date Picker Spin Button
Toolbar
@@ -842,6 +853,7 @@ Examples By Properties and States
diff --git a/examples/slider/css/slider-temperature.css b/examples/slider/css/slider-temperature.css
new file mode 100644
index 0000000000..55a2d52d7a
--- /dev/null
+++ b/examples/slider/css/slider-temperature.css
@@ -0,0 +1,107 @@
+/* CSS Document */
+
+.slider-valuetext h3 {
+ color: black;
+ font-weight: bold;
+ font-size: 150%;
+}
+
+.slider-temperature .label,
+.slider-seek .label {
+ font-weight: bold;
+ font-size: 125%;
+}
+
+.slider-temperature svg,
+.slider-seek svg {
+ forced-color-adjust: auto;
+}
+
+.slider-temperature text,
+.slider-seek text {
+ font-weight: bold;
+ fill: currentColor;
+ font-family: sans-serif;
+}
+
+.slider-temperature {
+ width: 12em;
+}
+
+.slider-temperature,
+.slider-seek {
+ margin-top: 1em;
+ padding: 6px;
+ color: black;
+}
+
+.slider-temperature .value,
+.slider-slider .value {
+ position: relative;
+ top: 20px;
+ height: 1.5em;
+ font-size: 80%;
+}
+
+.slider-temperature .temp-value,
+.slider-seek .temp-value {
+ padding-left: 24px;
+ font-size: 200%;
+}
+
+.slider-temperature .rail,
+.slider-seek .rail {
+ stroke: currentColor;
+ stroke-width: 2px;
+ fill: currentColor;
+ fill-opacity: 25%;
+}
+
+.slider-temperature .thumb,
+.slider-seek .thumb {
+ stroke-width: 0;
+ fill: currentColor;
+}
+
+.slider-temperature .focus-ring,
+.slider-seek .focus-ring {
+ stroke: currentColor;
+ stroke-opacity: 0;
+ fill-opacity: 0;
+ stroke-width: 3px;
+ display: none;
+}
+
+.slider-temperature .slider-group {
+ touch-action: pan-x;
+}
+
+.slider-seek .slider-group {
+ touch-action: pan-y;
+}
+
+.slider-seek .slider-group .value {
+ display: none;
+}
+
+/* Focus and hover styling */
+
+.slider-seek.focus [role="slider"],
+.slider-temperature.focus [role="slider"] {
+ color: #005a9c;
+}
+
+.slider-temperature [role="slider"]:focus,
+.slider-seek [role="slider"]:focus {
+ outline: none;
+}
+
+.slider-temperature [role="slider"]:focus .focus-ring,
+.slider-seek [role="slider"]:focus .focus-ring {
+ display: block;
+ stroke-opacity: 1;
+}
+
+.slider-seek [role="slider"]:focus .value {
+ display: block;
+}
diff --git a/examples/slider/js/slider-temperature.js b/examples/slider/js/slider-temperature.js
new file mode 100644
index 0000000000..9f4770c338
--- /dev/null
+++ b/examples/slider/js/slider-temperature.js
@@ -0,0 +1,256 @@
+/*
+ * This content is licensed according to the W3C Software License at
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
+ *
+ * File: slider-valuetext.js
+ *
+ * Desc: Slider widgets using aria-valuetext that implements ARIA Authoring Practices
+ */
+
+'use strict';
+
+class SliderTemperature {
+ constructor(domNode) {
+ this.labelCelsiusAbbrev = '°C';
+ this.labelCelsius = ' degrees Celsius';
+ this.changeValue = 0.1;
+ this.bigChangeValue = 20 * this.changeValue;
+
+ this.domNode = domNode;
+
+ this.isMoving = false;
+
+ this.svgNode = domNode.querySelector('svg');
+ this.svgPoint = this.svgNode.createSVGPoint();
+
+ this.borderWidth = 2;
+ this.borderWidth2 = 2 * this.borderWidth;
+
+ this.railNode = domNode.querySelector('.rail');
+ this.sliderNode = domNode.querySelector('[role=slider]');
+ this.sliderValueNode = this.sliderNode.querySelector('.value');
+ this.sliderFocusNode = this.sliderNode.querySelector('.focus-ring');
+ this.sliderThumbNode = this.sliderNode.querySelector('.thumb');
+
+ // The input elements are optional to support mobile devices,
+ // when a keyboard is not present
+ this.valueNode = domNode.querySelector('.temp-value');
+
+ // Dimensions of the slider focus ring, thumb and rail
+
+ this.valueX = parseInt(this.sliderValueNode.getAttribute('x'));
+ this.valueHeight = this.sliderValueNode.getBoundingClientRect().height;
+
+ this.railHeight = parseInt(this.railNode.getAttribute('height'));
+ this.railWidth = parseInt(this.railNode.getAttribute('width'));
+ this.railY = parseInt(this.railNode.getAttribute('y'));
+ this.railX = parseInt(this.railNode.getAttribute('x'));
+
+ this.thumbY = parseInt(this.sliderThumbNode.getAttribute('y'));
+ this.thumbWidth = parseInt(this.sliderThumbNode.getAttribute('width'));
+ this.thumbHeight = parseInt(this.sliderThumbNode.getAttribute('height'));
+
+ this.focusX = parseInt(this.sliderFocusNode.getAttribute('x'));
+ this.focusWidth = parseInt(this.sliderFocusNode.getAttribute('width'));
+ this.focusHeight = parseInt(this.sliderFocusNode.getAttribute('height'));
+
+ this.thumbX = this.railX + this.railWidth / 2 - this.thumbWidth / 2;
+ this.sliderThumbNode.setAttribute('x', this.thumbX);
+ this.sliderValueNode.setAttribute('x', this.valueX);
+ this.sliderFocusNode.setAttribute('x', this.focusX);
+
+ this.svgNode.addEventListener('click', this.onRailClick.bind(this));
+ this.sliderNode.addEventListener(
+ 'keydown',
+ this.onSliderKeydown.bind(this)
+ );
+ this.sliderNode.addEventListener(
+ 'pointerdown',
+ this.onSliderPointerDown.bind(this)
+ );
+
+ // bind a pointermove event handler to move pointer
+ this.svgNode.addEventListener('pointermove', this.onPointerMove.bind(this));
+
+ // bind a pointerup event handler to stop tracking pointer movements
+ document.addEventListener('pointerup', this.onPointerUp.bind(this));
+
+ this.sliderNode.addEventListener('focus', this.onSliderFocus.bind(this));
+ this.sliderNode.addEventListener('blur', this.onSliderBlur.bind(this));
+
+ this.moveSliderTo(this.getValue());
+ }
+
+ // Get point in global SVG space
+ getSVGPoint(event) {
+ this.svgPoint.x = event.clientX;
+ this.svgPoint.y = event.clientY;
+ return this.svgPoint.matrixTransform(this.svgNode.getScreenCTM().inverse());
+ }
+
+ getValue() {
+ return parseFloat(this.sliderNode.getAttribute('aria-valuenow'));
+ }
+
+ getValueMin() {
+ return parseFloat(this.sliderNode.getAttribute('aria-valuemin'));
+ }
+
+ getValueMax() {
+ return parseFloat(this.sliderNode.getAttribute('aria-valuemax'));
+ }
+
+ isInRange(value) {
+ let valueMin = this.getValueMin();
+ let valueMax = this.getValueMax();
+ return value <= valueMax && value >= valueMin;
+ }
+
+ moveSliderTo(value) {
+ var valueMax, valueMin, pos;
+
+ valueMin = this.getValueMin();
+ valueMax = this.getValueMax();
+
+ value = Math.min(Math.max(value, valueMin), valueMax);
+
+ let valueOutput = value.toFixed(1) + this.labelCelsiusAbbrev;
+
+ let valueText = value.toFixed(1) + this.labelCelsius;
+
+ this.valueNode.textContent = valueOutput;
+ this.sliderNode.setAttribute('aria-valuenow', value.toFixed(1));
+ this.sliderNode.setAttribute('aria-valuetext', valueText);
+
+ let height = this.railHeight - this.thumbHeight + this.borderWidth2;
+
+ pos = this.railY + height - 1;
+ pos -= Math.round(((value - valueMin) * height) / (valueMax - valueMin));
+ this.sliderNode.setAttribute('y', pos);
+
+ // update INPUT, label and ARIA attributes
+ this.sliderValueNode.textContent = valueOutput;
+
+ // move the SVG focus ring and thumb elements
+ this.sliderFocusNode.setAttribute(
+ 'y',
+ pos - (this.focusHeight - this.thumbHeight) / 2
+ );
+ this.sliderThumbNode.setAttribute('y', pos);
+
+ // Position value
+ this.sliderValueNode.setAttribute(
+ 'y',
+ pos -
+ this.borderWidth +
+ this.thumbHeight -
+ (this.valueHeight - this.thumbHeight) / 2
+ );
+ }
+
+ onSliderKeydown(event) {
+ var flag = false;
+ var value = this.getValue();
+ var valueMin = this.getValueMin();
+ var valueMax = this.getValueMax();
+
+ switch (event.key) {
+ case 'ArrowLeft':
+ case 'ArrowDown':
+ this.moveSliderTo(value - this.changeValue);
+ flag = true;
+ break;
+
+ case 'ArrowRight':
+ case 'ArrowUp':
+ this.moveSliderTo(value + this.changeValue);
+ flag = true;
+ break;
+
+ case 'PageDown':
+ this.moveSliderTo(value - this.bigChangeValue);
+ flag = true;
+ break;
+
+ case 'PageUp':
+ this.moveSliderTo(value + this.bigChangeValue);
+ flag = true;
+ break;
+
+ case 'Home':
+ this.moveSliderTo(valueMin);
+ flag = true;
+ break;
+
+ case 'End':
+ this.moveSliderTo(valueMax);
+ flag = true;
+ break;
+
+ default:
+ break;
+ }
+
+ if (flag) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ onSliderFocus() {
+ this.domNode.classList.add('focus');
+ }
+
+ onSliderBlur() {
+ this.domNode.classList.remove('focus');
+ }
+
+ calcValue(y) {
+ let min = this.getValueMin();
+ let max = this.getValueMax();
+ let diffY = y - this.railY;
+ return max - (diffY * (max - min)) / this.railHeight;
+ }
+
+ onRailClick(event) {
+ var value = this.calcValue(this.getSVGPoint(event).y);
+ this.moveSliderTo(value);
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Set focus to the clicked handle
+ this.sliderNode.focus();
+ }
+
+ onSliderPointerDown(event) {
+ this.isMoving = true;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Set focus to the clicked handle
+ this.sliderNode.focus();
+ }
+
+ onPointerMove(event) {
+ if (this.isMoving) {
+ var value = this.calcValue(this.getSVGPoint(event).y);
+ this.moveSliderTo(value);
+
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ onPointerUp() {
+ this.isMoving = false;
+ }
+}
+
+window.addEventListener('load', function () {
+ var sliders = document.querySelectorAll('.slider-temperature');
+ for (let i = 0; i < sliders.length; i++) {
+ new SliderTemperature(sliders[i]);
+ }
+});
diff --git a/examples/slider/slider-color-viewer.html b/examples/slider/slider-color-viewer.html
index 17fd7ee5b3..b278585962 100644
--- a/examples/slider/slider-color-viewer.html
+++ b/examples/slider/slider-color-viewer.html
@@ -43,10 +43,9 @@ Color Viewer Slider Example
Similar examples include: