diff --git a/.vnurc b/.vnurc index db2d5aa3f5..904e2b308b 100644 --- a/.vnurc +++ b/.vnurc @@ -8,5 +8,7 @@ An “img” element must have an “alt” attribute, except under certain cond # Ignoring aria-posinset and aria-setsize on role row Attribute “aria-posinset” not allowed on element “tr” at this point. Attribute “aria-setsize” not allowed on element “tr” at this point. +# Ignoring role meter +Bad value “meter” for attribute “role” on element “div”. # Deleted Section Archive The “longdesc” attribute on the “img” element is obsolete. Use a regular “a” element to link to the description. diff --git a/examples/meter/css/meter.css b/examples/meter/css/meter.css new file mode 100644 index 0000000000..75934ca1e8 --- /dev/null +++ b/examples/meter/css/meter.css @@ -0,0 +1,17 @@ + +[role=meter] { + padding: 2px; + width: 200px; + height: 40px; + border: 2px solid black; + border-radius: 5px; +} + +.fill { + width: 100%; + height: 100%; + box-sizing: border-box; + border: 2px solid black; + border-radius: 3px; + background-color: black; +} diff --git a/examples/meter/js/meter.js b/examples/meter/js/meter.js new file mode 100644 index 0000000000..440647584a --- /dev/null +++ b/examples/meter/js/meter.js @@ -0,0 +1,117 @@ +var Meter = function (element) { + this.rootEl = element; + this.fillEl = element.querySelector('.fill'); + + // set up min, max, and current value + var min = element.getAttribute('aria-valuemin'); + var max = element.getAttribute('aria-valuemax'); + var value = element.getAttribute('aria-valuenow'); + this._update(parseFloat(min), parseFloat(max), parseFloat(value)); +}; + +/* Private methods */ + +// returns a number representing a percentage between 0 - 100 +Meter.prototype._calculatePercentFill = function (min, max, value) { + if (typeof min !== 'number' || typeof max !== 'number' || typeof value !== 'number') { + return 0; + } + + return 100 * (value - min) / (max - min); +}; + +// returns an hsl color string between red and green +Meter.prototype._getColorValue = function (percent) { + // red is 0deg, green is 120deg in hsl + // if percent is 100, hue should be red, and if percent is 0, hue should be green + var hue = (percent / 100) * (0 - 120) + 120; + + return 'hsl(' + hue + ', 100%, 40%)'; +}; + +// no return value; updates the meter element +Meter.prototype._update = function (min, max, value) { + // update fill width + if (min !== this.min || max !== this.max || value !== this.value) { + var percentFill = this._calculatePercentFill(min, max, value); + this.fillEl.style.width = percentFill + '%'; + this.fillEl.style.color = this._getColorValue(percentFill); + } + + // update aria attributes + if (min !== this.min) { + this.min = min; + this.rootEl.setAttribute('aria-valuemin', min + ''); + } + + if (max !== this.max) { + this.max = max; + this.rootEl.setAttribute('aria-valuemax', max + ''); + } + + if (value !== this.value) { + this.value = value; + this.rootEl.setAttribute('aria-valuenow', value + ''); + } +}; + +/* Public methods */ + +// no return value; modifies the meter element based on a new value +Meter.prototype.setValue = function (value) { + if (typeof value !== 'number') { + value = parseFloat(value); + } + + if (!isNaN(value)) { + this._update(this.min, this.max, value); + } +}; + +/* Code for example page */ + +window.addEventListener('load', function () { + // helper function to randomize example meter value + function getRandomValue (min, max) { + var range = max - min; + return Math.floor((Math.random() * range) + min); + } + + // init meters + var meterEls = document.querySelectorAll('[role=meter]'); + var meters = []; + Array.prototype.slice.call(meterEls).forEach(function (meterEl) { + meters.push(new Meter(meterEl)); + }); + + // randomly update meter values + + // returns an id for setInterval + function playMeters () { + return window.setInterval(function () { + meters.forEach(function (meter) { + meter.setValue(Math.random() * 100); + }); + }, 5000); + } + + // start meters + var updateInterval = playMeters(); + + // play/pause meter updates + var playButton = document.querySelector('.play-meters'); + playButton.addEventListener('click', function () { + var isPaused = playButton.classList.contains('paused'); + + if (isPaused) { + updateInterval = playMeters(); + playButton.classList.remove('paused'); + playButton.innerHTML = 'Pause Updates'; + } + else { + clearInterval(updateInterval); + playButton.classList.add('paused'); + playButton.innerHTML = 'Start Updates'; + } + }); +}); diff --git a/examples/meter/meter.html b/examples/meter/meter.html new file mode 100644 index 0000000000..6ad91436a6 --- /dev/null +++ b/examples/meter/meter.html @@ -0,0 +1,142 @@ + + + +Meter Example | WAI-ARIA Authoring Practices 1.1 + + + + + + + + + + + + + + + + +
+

Meter Example

+

The following example of a CPU meter demonstrates the meter design pattern.

+ +
+

Example

+ + +
+

The value of this meter changes every 5 seconds. Use the pause button to stop changes.

+

Central Processing Unit (CPU) Usage

+

+ +

+
+ +
+
+ +
+
+

Keyboard Support

+

Not applicable.

+
+ +
+

Role, Property, State, and Tabindex Attributes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RoleAttributeElementUsage
meter + div + +
    +
  • Identifies the element as a meter.
  • +
+
aria-valuemin="NUMBER"divSpecifies the minimum value the meter can have.
aria-valuemax="NUMBER"divSpecifies the maximum value the meter can have.
aria-valuenow="NUMBER"divSpecifies the current value of the meter.
aria-labelledbydivIdentifies the element that provides the accessible name of the meter.
+
+ +
+

Javascript and CSS Source Code

+ + +
+ +
+

HTML Source Code

+ + + +
+ + + + +
+
+ + + + diff --git a/test/tests/meter_meter.js b/test/tests/meter_meter.js new file mode 100644 index 0000000000..bde3005ee7 --- /dev/null +++ b/test/tests/meter_meter.js @@ -0,0 +1,74 @@ +'use strict'; + +const { ariaTest } = require('..'); +const { By } = require('selenium-webdriver'); +const assertAriaLabelledby = require('../util/assertAriaLabelledby'); +const assertAriaRoles = require('../util/assertAriaRoles'); + +const exampleFile = 'meter/meter.html'; + +const ex = { + meterSelector: '#example [role="meter"]', + fillSelector: '#example [role="meter"] > svg' +}; + +// Attributes +ariaTest('role="meter" element exists', exampleFile, 'meter-role', async (t) => { + await assertAriaRoles(t, 'example', 'meter', '1', 'div'); +}); + +ariaTest('"aria-labelledby" attribute', exampleFile, 'meter-aria-labelledby', async (t) => { + await assertAriaLabelledby(t, ex.meterSelector); +}); + +ariaTest('"aria-valuemin" attribute', exampleFile, 'meter-aria-valuemin', async (t) => { + const meter = await t.context.session.findElement(By.css(ex.meterSelector)); + const valuemin = await meter.getAttribute('aria-valuemin'); + + t.is(typeof valuemin, 'string', 'aria-valuemin is present on the meter'); + t.false(isNaN(parseFloat(valuemin)), 'aria-valuemin is a number'); +}); + +ariaTest('"aria-valuemax" attribute', exampleFile, 'meter-aria-valuemax', async (t) => { + const meter = await t.context.session.findElement(By.css(ex.meterSelector)); + const [valuemax, valuemin] = await Promise.all([ + meter.getAttribute('aria-valuemax'), + meter.getAttribute('aria-valuemin') + ]); + + t.is(typeof valuemax, 'string', 'aria-valuemax is present on the meter'); + t.false(isNaN(parseFloat(valuemax)), 'aria-valuemax is a number'); + t.true(parseFloat(valuemax) >= parseFloat(valuemin), 'max value is greater than min value'); +}); + +ariaTest('"aria-valuenow" attribute', exampleFile, 'meter-aria-valuenow', async (t) => { + const meter = await t.context.session.findElement(By.css(ex.meterSelector)); + const [valuenow, valuemax, valuemin] = await Promise.all([ + meter.getAttribute('aria-valuenow'), + meter.getAttribute('aria-valuemax'), + meter.getAttribute('aria-valuemin') + ]); + + t.is(typeof valuenow, 'string', 'aria-valuenow is present on the meter'); + t.false(isNaN(parseFloat(valuenow)), 'aria-valuenow is a number'); + t.true(parseFloat(valuenow) >= parseFloat(valuemin), 'current value is greater than min value'); + t.true(parseFloat(valuenow) <= parseFloat(valuemax), 'current value is less than max value'); +}); + +ariaTest('fill matches current value', exampleFile, 'meter-aria-valuenow', async (t) => { + const meter = await t.context.session.findElement(By.css(ex.meterSelector)); + const fill = await t.context.session.findElement(By.css(ex.fillSelector)); + const [valuenow, valuemax, valuemin] = await Promise.all([ + meter.getAttribute('aria-valuenow'), + meter.getAttribute('aria-valuemax'), + meter.getAttribute('aria-valuemin') + ]); + + const currentPercent = (valuenow - valuemin) / (valuemax - valuemin); + const [fillSize, meterSize] = await Promise.all([fill.getRect(), meter.getRect()]); + + // fudging a little here, since meter has 8px total border + padding + // would be better in a unit test eventually + const expectedFillWidth = (meterSize.width - 8) * currentPercent; + t.true(Math.abs(expectedFillWidth - fillSize.width) < 10, 'Fill width is the correct percent of meter, +/- 10px'); +});