Skip to content

Commit

Permalink
feat(rule): New aria-input-field-label rule (#1610)
Browse files Browse the repository at this point in the history
* feat: initial implementation

* feat: rule aria-foorm-field-label with new check for no-implicit-explicit-label

* update rule

* test: fix lint errors

* add tests and updates based on review

* initial implementation

* remove has-visible-text check

* update tests
  • Loading branch information
jeeyyy authored and WilcoFiers committed Jun 6, 2019
1 parent b9699f6 commit 73d5273
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 0 deletions.
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| aria-dpub-role-fallback | Ensures unsupported DPUB roles are only used on elements with implicit fallback roles | Moderate | cat.aria, wcag2a, wcag131 | true |
| aria-hidden-body | Ensures aria-hidden='true' is not present on the document body. | Critical | cat.aria, wcag2a, wcag412 | true |
| aria-hidden-focus | Ensures aria-hidden elements do not contain focusable elements | Serious | cat.name-role-value, wcag2a, wcag412, wcag131 | true |
| aria-input-field-label | Ensures every ARIA input field has an accessible name | Moderate, Serious | wcag2a, wcag412 | true |
| aria-required-attr | Ensures elements with ARIA roles have all required ARIA attributes | Critical | cat.aria, wcag2a, wcag412 | true |
| aria-required-children | Ensures elements with an ARIA role that require child roles contain them | Critical | cat.aria, wcag2a, wcag131 | true |
| aria-required-parent | Ensures elements with an ARIA role that require parent roles are contained by them | Critical | cat.aria, wcag2a, wcag131 | true |
Expand Down
21 changes: 21 additions & 0 deletions lib/checks/aria/no-implicit-explicit-label.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { aria, text } = axe.commons;

const role = aria.getRole(node, { noImplicit: true });
this.data(role);

const labelText = text.sanitize(text.labelText(virtualNode)).toLowerCase();
const accText = text.sanitize(text.accessibleText(node)).toLowerCase();

if (!accText && !labelText) {
return false;
}

if (!accText && labelText) {
return undefined;
}

if (!accText.includes(labelText)) {
return undefined;
}

return false;
11 changes: 11 additions & 0 deletions lib/checks/aria/no-implicit-explicit-label.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "no-implicit-explicit-label",
"evaluate": "no-implicit-explicit-label.js",
"metadata": {
"impact": "moderate",
"messages": {
"pass": "There is no mismatch between a <label> and accessible name",
"incomplete": "Check that the <label> does not need be part of the ARIA {{=it.data}} field's name"
}
}
}
39 changes: 39 additions & 0 deletions lib/rules/aria-form-field-label-matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Note:
* This rule filters elements with 'role=*' attribute via 'selector'
* see relevant rule spec for details of 'role(s)' being filtered.
*/
const { aria } = axe.commons;

const nodeName = node.nodeName.toUpperCase();
const role = aria.getRole(node, { noImplicit: true });

/**
* Ignore elements from rule -> 'area-alt'
*/
if (nodeName === 'AREA' && !!node.getAttribute('href')) {
return false;
}

/**
* Ignore elements from rule -> 'label'
*/
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(nodeName)) {
return false;
}

/**
* Ignore elements from rule -> 'image-alt'
*/
if (nodeName === 'IMG' || (role === 'img' && nodeName !== 'SVG')) {
return false;
}

/**
* Ignore elements from rule -> 'button-name'
*/
if (nodeName === 'BUTTON' || role === 'button') {
return false;
}

return true;
13 changes: 13 additions & 0 deletions lib/rules/aria-input-field-label.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"id": "aria-input-field-label",
"selector": "[role=\"combobox\"], [role=\"listbox\"], [role=\"searchbox\"], [role=\"slider\"], [role=\"spinbutton\"], [role=\"textbox\"]",
"matches": "aria-form-field-label-matches.js",
"tags": ["wcag2a", "wcag412"],
"metadata": {
"description": "Ensures every ARIA input field has an accessible name",
"help": "ARIA input fields have an accessible name"
},
"all": [],
"any": ["aria-label", "aria-labelledby", "non-empty-title"],
"none": ["no-implicit-explicit-label"]
}
45 changes: 45 additions & 0 deletions test/checks/aria/no-implicit-explicit-label.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
describe('no-implicit-explicit-label', function() {
'use strict';

var fixture = document.getElementById('fixture');
var queryFixture = axe.testUtils.queryFixture;
var check = checks['no-implicit-explicit-label'];
var checkContext = axe.testUtils.MockCheckContext();

afterEach(function() {
fixture.innerHTML = '';
checkContext.reset();
});

it('returns false when there is no label text or accessible text', function() {
var vNode = queryFixture(
'<div id="target" role="searchbox" contenteditable="true"></div>'
);
var actual = check.evaluate.call(checkContext, vNode.actualNode, {}, vNode);
assert.isFalse(actual);
});

it('returns undefined when there is no accessible text', function() {
var vNode = queryFixture(
'<label for="target">Choose currency:</label><div id="target" role="searchbox" contenteditable="true"></div>'
);
var actual = check.evaluate.call(checkContext, vNode.actualNode, {}, vNode);
assert.isUndefined(actual);
});

it('returns undefined when accessible text does not contain label text', function() {
var vNode = queryFixture(
'<label for="target">Choose country:</label><div id="target" aria-label="country" role="combobox">England</div>'
);
var actual = check.evaluate.call(checkContext, vNode.actualNode, {}, vNode);
assert.isUndefined(actual);
});

it('returns false when accessible text contains label text', function() {
var vNode = queryFixture(
'<label for="target">Country</label><div id="target" aria-label="Choose country" role="combobox">England</div>'
);
var actual = check.evaluate.call(checkContext, vNode.actualNode, {}, vNode);
assert.isFalse(actual);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<!-- PASS -->
<!-- combobox -->
<div id="pass1" aria-label="country" role="combobox">England</div>
<!-- listbox -->
<p id="pass2Label">Select a color:</p>
<div id="pass2" role="listbox" aria-labelledby="pass2Label">
<div role="option">Orange</div>
</div>
<!-- searchbox -->
<p id="pass3Label">Search currency pairs:</p>
<div
id="pass3"
role="searchbox"
contenteditable="true"
aria-labelledby="pass3Label"
></div>
<!-- slider -->
<div
id="pass4"
role="slider"
aria-label="Choose a value"
aria-valuemin="1"
aria-valuemax="7"
aria-valuenow="2"
></div>
<!-- spinbutton -->
<div
id="pass5"
role="spinbutton"
aria-valuemin="0"
aria-valuemax="10"
aria-valuenow="8"
aria-label="Enter quantity:"
></div>
<!-- textbox -->
<label id="foo">
foo
<div id="pass6" role="textbox" aria-labelledby="foo"></div>
</label>

<!-- FAIL -->
<!-- aria-label with empty text string -->
<div id="fail1" aria-label=" " role="combobox">England</div>
<!-- The label does not exist. -->
<div id="fail2" aria-labelledby="non-existing" role="combobox">England</div>
<!-- The implicit label is not supported on div elements. -->
<label>
first name
<div id="fail3" role="textbox"></div>
</label>
<!-- explicit label -->
<label for="fail4">first name</label>
<div role="textbox" id="fail4"></div>
<!-- combobox -->
<div id="fail5" role="combobox">England</div>
<!-- listbox -->
<div id="fail6" role="listbox" aria-labelledby="label-does-not-exist">
<div role="option">Orange</div>
</div>
<!-- searchbox -->
<div
id="fail7"
role="searchbox"
contenteditable="true"
aria-labelledby="unknown-label"
></div>
<!-- slider -->
<div
id="fail8"
role="slider"
aria-valuemin="1"
aria-valuemax="7"
aria-valuenow="2"
></div>
<!-- spinbutton -->
<div
id="fail9"
role="spinbutton"
aria-valuemin="0"
aria-valuemax="10"
aria-valuenow="8"
></div>
<!-- textbox -->
<label>
foo
<div id="fail10" role="textbox"></div>
</label>

<!-- INAPPLICABLE -->
<input id="inapplicable1" />
<select id="inapplicable2">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="opel">Opel</option>
</select>
<textarea id="inapplicable3" title="Label"></textarea>

<!-- INCOMPLETE -->
<!-- Implicit label -->
<label>
first name
<div id="canttell1" role="textbox" aria-label="name"></div>
</label>

<!-- Explicit label -->
<label for="canttell2">first name</label>
<div role="textbox" id="canttell2" aria-label="name"></div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"description": "aria-input-field-label test",
"rule": "aria-input-field-label",
"passes": [
["#pass1"],
["#pass2"],
["#pass3"],
["#pass4"],
["#pass5"],
["#pass6"]
],
"violations": [
["#fail1"],
["#fail2"],
["#fail3"],
["#fail4"],
["#fail5"],
["#fail6"],
["#fail7"],
["#fail8"],
["#fail9"],
["#fail10"]
],
"incomplete": [["#canttell1"], ["#canttell2"]]
}
68 changes: 68 additions & 0 deletions test/rule-matches/aria-form-field-label-matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
describe('aria-form-field-label-matches', function() {
'use strict';

var fixture = document.getElementById('fixture');
var queryFixture = axe.testUtils.queryFixture;
var rule = axe._audit.rules.find(function(rule) {
return (
rule.id === 'aria-toggle-field-label' ||
rule.id === 'aria-input-field-label'
);
});

afterEach(function() {
fixture.innerHTML = '';
});

it('returns false for node `map area[href]`', function() {
var vNode = queryFixture(
'<map><area id="target" href="#" role="checkbox"></map>'
);
var actual = rule.matches(vNode.actualNode, vNode);
assert.isFalse(actual);
});

it('returns false when node is either INPUT, SELECT or TEXTAREA', function() {
['INPUT', 'SELECT', 'TEXTAREA'].forEach(function(node) {
var vNode = queryFixture(
'<' + node + ' role="menuitemcheckbox" id="target"><' + node + '>'
);
var actual = rule.matches(vNode.actualNode, vNode);
assert.isFalse(actual);
});
});

it('returns false when node is IMG', function() {
var vNode = queryFixture('<img id="target" role="menuitemradio">');
var actual = rule.matches(vNode.actualNode, vNode);
assert.isFalse(actual);
});

it('returns false when node is not SVG with role=`img`', function() {
var vNode = queryFixture('<div id="target" role="img">');
var actual = rule.matches(vNode.actualNode, vNode);
assert.isFalse(actual);
});

it('returns false when node is BUTTON', function() {
var vNode = queryFixture('<button id="target" role="button"></button>');
var actual = rule.matches(vNode.actualNode, vNode);
assert.isFalse(actual);
});

it('returns false when role=`button`', function() {
var vNode = queryFixture('<div id="target" role="button"></div>');
var actual = rule.matches(vNode.actualNode, vNode);
assert.isFalse(actual);
});

it('returns false for INPUT of type `BUTTON`, `SUBMIT` or `RESET`', function() {
['button', 'submit', 'reset'].forEach(function(type) {
var vNode = queryFixture(
'<input id="target" role="radio" type="' + type + '">'
);
var actual = rule.matches(vNode.actualNode, vNode);
assert.isFalse(actual);
});
});
});

0 comments on commit 73d5273

Please sign in to comment.