diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index 27824fc4f4..f72bad79d7 100644 --- a/lib/checks/keyboard/no-focusable-content-evaluate.js +++ b/lib/checks/keyboard/no-focusable-content-evaluate.js @@ -1,4 +1,5 @@ import isFocusable from '../../commons/dom/is-focusable'; +import { getRole, getRoleType } from '../../commons/aria'; export default function noFocusableContentEvaluate(node, options, virtualNode) { if (!virtualNode.children) { @@ -40,7 +41,8 @@ function getFocusableDescendants(vNode) { const retVal = []; vNode.children.forEach(child => { - if (isFocusable(child)) { + const role = getRole(child); + if (getRoleType(role) === 'widget' && isFocusable(child)) { retVal.push(child); } else { retVal.push(...getFocusableDescendants(child)); diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index 1389e0241a..d642bbd15e 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -28,17 +28,32 @@ describe('no-focusable-content tests', function() { assert.isTrue(noFocusableContent(null, null, vNode)); }); - it('should return false if element has focusable content', function() { + it('should return true if element has content which is focusable (tabindex=0) and does not have a widget role', function() { var params = checkSetup( '' ); - assert.isFalse(noFocusableContent.apply(checkContext, params)); - assert.deepEqual(checkContext._data, null); + assert.isTrue(noFocusableContent.apply(checkContext, params)); + }); + + it('should return true if element has content which has negative tabindex and non-widget role', function() { + var vNode = queryFixture( + '' + ); + assert.isTrue(noFocusableContent(null, null, vNode)); + }); + + it('should return false if element has content which has negative tabindex and an explicit widget role', function() { + var params = checkSetup( + '' + ); + axe.utils.getFlattenedTree(document.documentElement); + assert.isFalse(check.evaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { messageKey: 'notHidden' }); assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); - it('should return false if element has natively focusable content', function() { + it('should return false if element has content which is natively focusable and has a widget role', function() { var params = checkSetup( '' ); @@ -50,7 +65,7 @@ describe('no-focusable-content tests', function() { it('should add each focusable child as related nodes', function() { var params = checkSetup( - '' + '' ); assert.isFalse(noFocusableContent.apply(checkContext, params)); @@ -61,7 +76,7 @@ describe('no-focusable-content tests', function() { ]); }); - it('should return false if element has natively focusable content with negative tabindex', function() { + it('should return false if element has natively focusable widget role content with negative tabindex', function() { var params = checkSetup( '' ); @@ -70,4 +85,33 @@ describe('no-focusable-content tests', function() { assert.deepEqual(checkContext._data, { messageKey: 'notHidden' }); assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); + + it('should return true if element has content which is natively focusable and has a widget role but is disabled', function() { + var vNode = queryFixture( + '' + ); + assert.isTrue(noFocusableContent(null, null, vNode)); + }); + + it('should return true on span with negative tabindex (focusable, does not have a widget role)', function() { + var vNode = queryFixture(' some text ' + +'JavaScript is able to focus this ' + +''); + assert.isTrue(noFocusableContent(null, null, vNode)); + }); + + it('should return true on aria-hidden span with negative tabindex (focusable, does not have a widget role)', function() { + var vNode = queryFixture(' some text ' + +' ' + +''); + assert.isTrue(noFocusableContent(null, null, vNode)); + }); + + it('should return true on nested span with tabindex=0 (focusable, does not have a widget role)', function() { + var vNode = queryFixture(' some text ' + +'anyone is able to focus this ' + +''); + assert.isTrue(noFocusableContent(null, null, vNode)); + }); + }); diff --git a/test/integration/rules/aria-text/aria-text.html b/test/integration/rules/aria-text/aria-text.html index 28252c0bce..54809b6bd5 100644 --- a/test/integration/rules/aria-text/aria-text.html +++ b/test/integration/rules/aria-text/aria-text.html @@ -20,3 +20,11 @@

explicit role.

+

+

Hello

+
+ passes because no href makes this not have the implicit role of 'link' +
+
+ passes because no href makes this not have the implicit role of 'link' +
diff --git a/test/integration/rules/aria-text/aria-text.json b/test/integration/rules/aria-text/aria-text.json index f6c3f1d2d9..9443502a16 100644 --- a/test/integration/rules/aria-text/aria-text.json +++ b/test/integration/rules/aria-text/aria-text.json @@ -1,6 +1,6 @@ { "description": "aria-text tests", "rule": "aria-text", - "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"]], - "passes": [["#pass1"], ["#pass2"], ["#pass3"]] + "violations": [["#fail1"], ["#fail2"], ["#fail3"], ["#fail4"], ["#fail5"]], + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"], ["#pass6"]] } diff --git a/test/integration/rules/nested-interactive/nested-interactive.html b/test/integration/rules/nested-interactive/nested-interactive.html index fb42dd24dc..1af908b261 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.html +++ b/test/integration/rules/nested-interactive/nested-interactive.html @@ -1,16 +1,18 @@ -
pass
- - - + +
pass
+ + + + - -
- - - - - + +
+ + + + + ignored ignored diff --git a/test/integration/rules/nested-interactive/nested-interactive.json b/test/integration/rules/nested-interactive/nested-interactive.json index 434b22bb90..4db1a12cdb 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.json +++ b/test/integration/rules/nested-interactive/nested-interactive.json @@ -6,9 +6,7 @@ ["#fail2"], ["#fail3"], ["#fail4"], - ["#fail5"], - ["#fail6"], - ["#fail7"] + ["#fail5"] ], "passes": [ ["#pass1"], @@ -17,6 +15,10 @@ ["#pass4"], ["#pass5"], ["#pass6"], - ["#pass7"] + ["#pass7"], + ["#pass8"], + ["#pass9"], + ["#pass10"], + ["#pass11"] ] } diff --git a/test/integration/virtual-rules/nested-interactive.js b/test/integration/virtual-rules/nested-interactive.js index ba963b99b5..acdb54f389 100644 --- a/test/integration/virtual-rules/nested-interactive.js +++ b/test/integration/virtual-rules/nested-interactive.js @@ -38,6 +38,26 @@ describe('nested-interactive virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); + it('should pass for element with non-widget content which has negative tabindex', 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, 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', @@ -54,7 +74,7 @@ describe('nested-interactive virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should fail for element with focusable content', function() { + it('should pass for element with non-widget content', function() { var node = new axe.SerialVirtualNode({ nodeName: 'button' }); @@ -69,12 +89,12 @@ describe('nested-interactive virtual-rule', function() { var results = axe.runVirtualRule('nested-interactive', node); - assert.lengthOf(results.passes, 0); - assert.lengthOf(results.violations, 1); + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); assert.lengthOf(results.incomplete, 0); }); - it('should fail for element with natively focusable content', function() { + it('should fail for element with native widget content', function() { var node = new axe.SerialVirtualNode({ nodeName: 'div', attributes: {