Skip to content

Commit

Permalink
Add meter example for issue 1105 (pull #1190)
Browse files Browse the repository at this point in the history
Resolves #1105 by adding a CPU meter display that implements the meter pattern.
  • Loading branch information
a11ydoer authored and mcking65 committed Oct 24, 2019
1 parent 514af61 commit 96e5afd
Show file tree
Hide file tree
Showing 5 changed files with 352 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .vnurc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
17 changes: 17 additions & 0 deletions examples/meter/css/meter.css
Original file line number Diff line number Diff line change
@@ -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;
}
117 changes: 117 additions & 0 deletions examples/meter/js/meter.js
Original file line number Diff line number Diff line change
@@ -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';
}
});
});
142 changes: 142 additions & 0 deletions examples/meter/meter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Meter Example | WAI-ARIA Authoring Practices 1.1</title>
<!-- Core js and css shared by all examples; do not modify when using this template. -->
<link rel="stylesheet" href="https://www.w3.org/StyleSheets/TR/2016/base.css">
<link rel="stylesheet" href="../css/core.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">

<script src="../js/examples.js" type="text/javascript"></script>
<script src="../js/highlight.pack.js"></script>
<script src="../js/app.js"></script>

<!-- CSS and JS for this example. -->
<link rel="stylesheet" href="css/meter.css">
<script src="js/meter.js" type="text/javascript"></script>

</head>
<body>
<nav aria-label="Related Links" class="feedback">
<ul>
<li><a href="../../../#browser_and_AT_support">Browser and Assistive Technology Support</a></li>
<li><a href="https://github.com/w3c/aria-practices/issues/new">Report Issue</a></li>
<li><a href="https://github.com/w3c/aria-practices/projects/30">Related Issues</a></li>
<li><a href="../../#meter">Design Pattern</a></li>
</ul>
</nav>
<main>
<h1> Meter Example</h1>
<p>The following example of a CPU meter demonstrates the <a href="../../#meter">meter design pattern</a>.</p>

<section>
<h2 id="ex_label">Example</h2>
<div role="separator" id="ex_start_sep" aria-labelledby="ex_start_sep ex_label" aria-label="Start of"></div>

<div id="example">
<p>The value of this meter changes every 5 seconds. Use the pause button to stop changes.</p>
<h3 id="cpu_usage_label">Central Processing Unit (CPU) Usage</h3>
<p>
<button type="button" class="play-meters">Pause Updates</button>
</p>
<div role="meter" aria-valuenow="90" aria-valuemin="0" aria-valuemax="100" aria-labelledby="cpu_usage_label">
<svg width="100" height="100" class="fill" aria-hidden="true" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="100%" height="100%" fill="currentColor" />
</svg>
</div>
</div>
<div role="separator" id="ex_end_sep" aria-labelledby="ex_end_sep ex_label" aria-label="End of"></div>
</section>
<section>
<h2 id="kbd_label">Keyboard Support</h2>
<p>Not applicable.</p>
</section>

<section>
<h2 id="rps_label">Role, Property, State, and Tabindex Attributes</h2>
<table aria-labelledby="rps_label" class="data attributes">
<thead>
<tr>
<th scope="col">Role</th>
<th scope="col">Attribute</th>
<th scope="col">Element</th>
<th scope="col">Usage</th>
</tr>
</thead>
<tbody>
<tr data-test-id="meter-role">
<th scope="row"><code>meter</code></th>
<td></td>
<td>
<code>div</code>
</td>
<td>
<ul>
<li>Identifies the element as a <code>meter</code>.</li>
</ul>
</td>
</tr>
<tr data-test-id="meter-aria-valuemin">
<td></td>
<th scope="row"><code>aria-valuemin="NUMBER"</code></th>
<td><code>div</code></td>
<td>Specifies the minimum value the meter can have.</td>
</tr>
<tr data-test-id="meter-aria-valuemax">
<td></td>
<th scope="row"><code>aria-valuemax="NUMBER"</code></th>
<td><code>div</code></td>
<td>Specifies the maximum value the meter can have.</td>
</tr>
<tr data-test-id="meter-aria-valuenow">
<td></td>
<th scope="row"><code>aria-valuenow="NUMBER"</code></th>
<td><code>div</code></td>
<td>Specifies the current value of the meter.</td>
</tr>
<tr data-test-id="meter-aria-labelledby">
<td></td>
<th scope="row"><code>aria-labelledby</code></th>
<td><code>div</code></td>
<td>Identifies the element that provides the accessible name of the <code>meter</code>.</td>
</tr>
</tbody>
</table>
</section>

<section>
<h2>Javascript and CSS Source Code</h2>

<ul>
<li>
CSS:
<a href="css/meter.css" type="text/css">meter.css</a>
</li>
<li>
Javascript:
<a href="js/meter.js" type="text/javascript">meter.js</a>
</li>
</ul>
</section>

<section>
<h2 id="sc1_label">HTML Source Code</h2>

<div role="separator" id="sc1_start_sep" aria-labelledby="sc1_start_sep sc1_label" aria-label="Start of"></div>

<pre><code id="source1"></code></pre>

<div role="separator" id="sc1_end_sep" aria-labelledby="sc1_end_sep sc1_label" aria-label="End of"></div>

<script>
sourceCode.add('source1', 'example');
sourceCode.make();
</script>
</section>
</main>

<nav>
<a href="../../#meter">Meter Design Pattern in WAI-ARIA Authoring Practices 1.1</a>
</nav>
</body>
</html>
74 changes: 74 additions & 0 deletions test/tests/meter_meter.js
Original file line number Diff line number Diff line change
@@ -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');
});

0 comments on commit 96e5afd

Please sign in to comment.