-
Notifications
You must be signed in to change notification settings - Fork 795
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nested-interactive): new rule to flag nested interactive elements (
#2691) * feat(nested-interactive): new rule to flag nested interactive elements * Update lib/rules/nested-interactive.json Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com> * Update lib/rules/nested-interactive.json Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com> * fixes * remove only * remove log Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
- Loading branch information
1 parent
f2a2ff6
commit 13a7cf1
Showing
13 changed files
with
315 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,35 @@ | ||
import isFocusable from '../../commons/dom/is-focusable'; | ||
|
||
function focusableDescendants(vNode) { | ||
if (isFocusable(vNode)) { | ||
return true; | ||
} | ||
|
||
if (!vNode.children) { | ||
if (vNode.props.nodeType === 1) { | ||
throw new Error('Cannot determine children'); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
return vNode.children.some(child => { | ||
return focusableDescendants(child); | ||
}); | ||
} | ||
|
||
function noFocusbleContentEvaluate(node, options, virtualNode) { | ||
if (!virtualNode.children) { | ||
return undefined; | ||
} | ||
|
||
try { | ||
return !virtualNode.children.some(child => { | ||
return focusableDescendants(child); | ||
}); | ||
} catch (e) { | ||
return undefined; | ||
} | ||
} | ||
|
||
export default noFocusbleContentEvaluate; |
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,12 @@ | ||
{ | ||
"id": "no-focusable-content", | ||
"evaluate": "no-focusable-content-evaluate", | ||
"metadata": { | ||
"impact": "serious", | ||
"messages": { | ||
"pass": "Element does not have focusable descendants", | ||
"fail": "Element has focusable descendants", | ||
"incomplete": "Could not determine if element has descendants" | ||
} | ||
} | ||
} |
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
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 @@ | ||
import { getRole } from '../commons/aria'; | ||
import standards from '../standards'; | ||
|
||
function nestedInteractiveMatches(node, virtualNode) { | ||
const role = getRole(virtualNode); | ||
if (!role) { | ||
return false; | ||
} | ||
|
||
return !!standards.ariaRoles[role].childrenPresentational; | ||
} | ||
|
||
export default nestedInteractiveMatches; |
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,12 @@ | ||
{ | ||
"id": "nested-interactive", | ||
"matches": "nested-interactive-matches", | ||
"tags": ["cat.keyboard", "wcag2a", "wcag412"], | ||
"metadata": { | ||
"description": "Nested interactive controls are not announced by screen readers", | ||
"help": "Ensure interactive controls are not nested" | ||
}, | ||
"all": [], | ||
"any": ["no-focusable-content"], | ||
"none": [] | ||
} |
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,40 @@ | ||
describe('no-focusable-content tests', function() { | ||
var fixture = document.querySelector('#fixture'); | ||
var queryFixture = axe.testUtils.queryFixture; | ||
var noFocusableContent = axe.testUtils.getCheckEvaluate( | ||
'no-focusable-content' | ||
); | ||
|
||
afterEach(function() { | ||
fixture.innerHTML = ''; | ||
}); | ||
|
||
it('should return true if element has no focusable content', function() { | ||
var vNode = queryFixture('<button id="target"><span>Hello</span></button>'); | ||
assert.isTrue(noFocusableContent(null, null, vNode)); | ||
}); | ||
|
||
it('should return true if element is empty', function() { | ||
var vNode = queryFixture('<button id="target"></button>'); | ||
assert.isTrue(noFocusableContent(null, null, vNode)); | ||
}); | ||
|
||
it('should return true if element only has text content', function() { | ||
var vNode = queryFixture('<button id="target">Hello</button>'); | ||
assert.isTrue(noFocusableContent(null, null, vNode)); | ||
}); | ||
|
||
it('should return false if element has focusable content', function() { | ||
var vNode = queryFixture( | ||
'<button id="target"><span tabindex="0">Hello</span></button>' | ||
); | ||
assert.isFalse(noFocusableContent(null, null, vNode)); | ||
}); | ||
|
||
it('should return false if element has natively focusable content', function() { | ||
var vNode = queryFixture( | ||
'<button id="target"><a href="foo.html">Hello</a></button>' | ||
); | ||
assert.isFalse(noFocusableContent(null, null, vNode)); | ||
}); | ||
}); |
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
14 changes: 14 additions & 0 deletions
14
test/integration/rules/nested-interactive/nested-interactive.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,14 @@ | ||
<button id="pass1">pass</button> | ||
<div role="button" id="pass2">pass</div> | ||
<div role="tab" id="pass3">pass</div> | ||
<div role="checkbox" id="pass4">pass</div> | ||
<div role="radio" id="pass5"><span>pass</span></div> | ||
|
||
<button id="fail1"><span tabindex="0">fail</span></button> | ||
<div role="button" id="fail2"><input /></div> | ||
<div role="tab" id="fail3"><button id="pass6">fail</button></div> | ||
<div role="checkbox" id="fail4"><a href="foo.html">fail</a></div> | ||
<div role="radio" id="fail5"><span tabindex="0">fail</span></div> | ||
|
||
<a id="ignored1" href="foo.html">ignored</a> | ||
<span id="ignored2">ignored</span> |
13 changes: 13 additions & 0 deletions
13
test/integration/rules/nested-interactive/nested-interactive.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,13 @@ | ||
{ | ||
"description": "nested-interactive tests", | ||
"rule": "nested-interactive", | ||
"violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]], | ||
"passes": [ | ||
["#pass1"], | ||
["#pass2"], | ||
["#pass3"], | ||
["#pass4"], | ||
["#pass5"], | ||
["#pass6"] | ||
] | ||
} |
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,124 @@ | ||
describe('nested-interactive virtual-rule', function() { | ||
it('should pass for element without focusable content', function() { | ||
var node = new axe.SerialVirtualNode({ | ||
nodeName: 'button' | ||
}); | ||
var child = new axe.SerialVirtualNode({ | ||
nodeName: '#text', | ||
nodeType: 3, | ||
nodeValue: 'Hello World' | ||
}); | ||
node.children = [child]; | ||
|
||
var results = axe.runVirtualRule('nested-interactive', node); | ||
|
||
assert.lengthOf(results.passes, 1); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('should pass for aria element without focusable content', function() { | ||
var node = new axe.SerialVirtualNode({ | ||
nodeName: 'div', | ||
attributes: { | ||
role: 'button' | ||
} | ||
}); | ||
var child = new axe.SerialVirtualNode({ | ||
nodeName: '#text', | ||
nodeType: 3, | ||
nodeValue: 'Hello World' | ||
}); | ||
node.children = [child]; | ||
|
||
var results = axe.runVirtualRule('nested-interactive', node); | ||
|
||
assert.lengthOf(results.passes, 1); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('should pass for empty element without', function() { | ||
var node = new axe.SerialVirtualNode({ | ||
nodeName: 'div', | ||
attributes: { | ||
role: 'button' | ||
} | ||
}); | ||
node.children = []; | ||
|
||
var results = axe.runVirtualRule('nested-interactive', node); | ||
|
||
assert.lengthOf(results.passes, 1); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('should fail for element with focusable content', function() { | ||
var node = new axe.SerialVirtualNode({ | ||
nodeName: 'button' | ||
}); | ||
var child = new axe.SerialVirtualNode({ | ||
nodeName: 'span', | ||
attributes: { | ||
tabindex: 1 | ||
} | ||
}); | ||
child.children = []; | ||
node.children = [child]; | ||
|
||
var results = axe.runVirtualRule('nested-interactive', node); | ||
|
||
assert.lengthOf(results.passes, 0); | ||
assert.lengthOf(results.violations, 1); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('should fail for element with natively focusable content', function() { | ||
var node = new axe.SerialVirtualNode({ | ||
nodeName: 'div', | ||
attributes: { | ||
role: 'button' | ||
} | ||
}); | ||
var child = new axe.SerialVirtualNode({ | ||
nodeName: 'button' | ||
}); | ||
child.children = []; | ||
node.children = [child]; | ||
|
||
var results = axe.runVirtualRule('nested-interactive', node); | ||
|
||
assert.lengthOf(results.passes, 0); | ||
assert.lengthOf(results.violations, 1); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('should return incomplete if element has undefined children', function() { | ||
var node = new axe.SerialVirtualNode({ | ||
nodeName: 'button' | ||
}); | ||
|
||
var results = axe.runVirtualRule('nested-interactive', node); | ||
|
||
assert.lengthOf(results.passes, 0); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 1); | ||
}); | ||
|
||
it('should return incomplete if descendant has undefined children', function() { | ||
var node = new axe.SerialVirtualNode({ | ||
nodeName: 'button' | ||
}); | ||
var child = new axe.SerialVirtualNode({ | ||
nodeName: 'span' | ||
}); | ||
node.children = [child]; | ||
|
||
var results = axe.runVirtualRule('nested-interactive', node); | ||
|
||
assert.lengthOf(results.passes, 0); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 1); | ||
}); | ||
}); |
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,35 @@ | ||
describe('nested-interactive-matches', function() { | ||
var fixture = document.querySelector('#fixture'); | ||
var queryFixture = axe.testUtils.queryFixture; | ||
var rule; | ||
|
||
beforeEach(function() { | ||
rule = axe._audit.rules.find(function(rule) { | ||
return rule.id === 'nested-interactive'; | ||
}); | ||
}); | ||
|
||
afterEach(function() { | ||
fixture.innerHTML = ''; | ||
}); | ||
|
||
it('should match if element has children presentational', function() { | ||
var vNode = queryFixture('<button id="target"></button>'); | ||
assert.isTrue(rule.matches(null, vNode)); | ||
}); | ||
|
||
it('should match if aria element has children presentational', function() { | ||
var vNode = queryFixture('<div role="button" id="target"></div>'); | ||
assert.isTrue(rule.matches(null, vNode)); | ||
}); | ||
|
||
it('should not match if element does not have children presentational', function() { | ||
var vNode = queryFixture('<a href="foo.html" id="target"></a>'); | ||
assert.isFalse(rule.matches(null, vNode)); | ||
}); | ||
|
||
it('should not match if element has no role', function() { | ||
var vNode = queryFixture('<span id="target"></span>'); | ||
assert.isFalse(rule.matches(null, vNode)); | ||
}); | ||
}); |