-
Notifications
You must be signed in to change notification settings - Fork 783
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rule): New aria-input-field-label rule (#1610)
* 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
1 parent
b9699f6
commit 73d5273
Showing
9 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
107 changes: 107 additions & 0 deletions
107
test/integration/rules/aria-input-field-label/aria-input-field-label.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
25 changes: 25 additions & 0 deletions
25
test/integration/rules/aria-input-field-label/aria-input-field-label.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |