From 2467b3a59cb853689b88a15580acd6e9b210dd6b Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Thu, 19 Aug 2021 21:12:04 -0400 Subject: [PATCH 01/21] refactor(checks/navigation): improve `internal-link-present-evaluate` Make `internal-link-present-evaluate` work with virtualNode rather than actualNode. Closes issue #2466 --- .../internal-link-present-evaluate.js | 20 +-- .../navigation/internal-link-present.js | 155 +++++++++--------- 2 files changed, 88 insertions(+), 87 deletions(-) diff --git a/lib/checks/navigation/internal-link-present-evaluate.js b/lib/checks/navigation/internal-link-present-evaluate.js index acf086d43f..d235bfbe2b 100644 --- a/lib/checks/navigation/internal-link-present-evaluate.js +++ b/lib/checks/navigation/internal-link-present-evaluate.js @@ -1,10 +1,10 @@ -import { querySelectorAll } from '../../core/utils'; - -function internalLinkPresentEvaluate(node, options, virtualNode) { - const links = querySelectorAll(virtualNode, 'a[href]'); - return links.some(vLink => { - return /^#[^/!]/.test(vLink.actualNode.getAttribute('href')); - }); -} - -export default internalLinkPresentEvaluate; +import { querySelectorAll } from '../../core/utils'; + +function internalLinkPresentEvaluate(node, options, virtualNode) { + const links = querySelectorAll(virtualNode, 'a[href]'); + return links.some(vLink => { + return /^#[^/!]/.test(vLink.attr('href')); + }); +} + +export default internalLinkPresentEvaluate; diff --git a/test/checks/navigation/internal-link-present.js b/test/checks/navigation/internal-link-present.js index 5ea7efed12..2f3d34d561 100644 --- a/test/checks/navigation/internal-link-present.js +++ b/test/checks/navigation/internal-link-present.js @@ -1,77 +1,78 @@ -describe('internal-link-present', function() { - 'use strict'; - - var fixture = document.getElementById('fixture'); - var shadowSupported = axe.testUtils.shadowSupport.v1; - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var shadowCheckSetup = axe.testUtils.shadowCheckSetup; - - afterEach(function() { - fixture.innerHTML = ''; - axe._tree = undefined; - checkContext.reset(); - }); - - it('should return true when an internal link is found', function() { - var params = checkSetup('
hi
'); - assert.isTrue( - axe.testUtils - .getCheckEvaluate('internal-link-present') - .apply(checkContext, params) - ); - }); - - it('should return false when a hashbang URL was used', function() { - var params = checkSetup('
hi
'); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('internal-link-present') - .apply(checkContext, params) - ); - }); - - it('should return false when a hash route URL was used', function() { - var params = checkSetup('
hi
'); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('internal-link-present') - .apply(checkContext, params) - ); - }); - - it('should return false when a hashbang + slash route URL was used', function() { - var params = checkSetup('
hi
'); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('internal-link-present') - .apply(checkContext, params) - ); - }); - - it('should otherwise return false', function() { - var params = checkSetup( - '
hi
' - ); - assert.isFalse( - axe.testUtils - .getCheckEvaluate('internal-link-present') - .apply(checkContext, params) - ); - }); - - (shadowSupported ? it : xit)( - 'should return true when internal link is found in shadow dom', - function() { - var params = shadowCheckSetup( - '
', - 'hi' - ); - assert.isTrue( - axe.testUtils - .getCheckEvaluate('internal-link-present') - .apply(checkContext, params) - ); - } - ); -}); +describe('internal-link-present', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var shadowSupported = axe.testUtils.shadowSupport.v1; + var checkContext = axe.testUtils.MockCheckContext(); + var shadowCheckSetup = axe.testUtils.shadowCheckSetup; + var queryFixture = axe.testUtils.queryFixture; + + afterEach(function() { + fixture.innerHTML = ''; + axe._tree = undefined; + checkContext.reset(); + }); + + it('should return true when an internal link is found', function() { + var vNode = queryFixture('
hi
'); + assert.isTrue( + axe.testUtils + .getCheckEvaluate('internal-link-present') + .call(checkContext, null, {}, vNode) + ); + }); + + it('should return false when a hashbang URL was used', function() { + var vNode = queryFixture('
hi
'); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('internal-link-present') + .call(checkContext, null, {}, vNode) + ); + }); + + it('should return false when a hash route URL was used', function() { + var vNode = queryFixture('
hi
'); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('internal-link-present') + .call(checkContext, null, {}, vNode) + ); + }); + + it('should return false when a hashbang + slash route URL was used', function() { + var vNode = queryFixture('
hi
'); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('internal-link-present') + .call(checkContext, null, {}, vNode) + ); + }); + + it('should otherwise return false', function() { + var vNode = queryFixture( + '
hi
' + ); + assert.isFalse( + axe.testUtils + .getCheckEvaluate('internal-link-present') + .call(checkContext, null, {}, vNode) + ); + }); + + (shadowSupported ? it : xit)( + 'should return true when internal link is found in shadow dom', + function() { + var params = shadowCheckSetup( + '
', + 'hi' + ); + var vNode = params[2]; + assert.isTrue( + axe.testUtils + .getCheckEvaluate('internal-link-present') + .call(checkContext, null, {}, vNode) + ); + } + ); +}); From 9f996bc6f54092541db77b9c3e62e2a5d9d9643e Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 3 Sep 2021 08:48:36 -0400 Subject: [PATCH 02/21] test commit 1 --- test/testutils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/testutils.js b/test/testutils.js index 8c60d13a93..5b37f8b83d 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -501,3 +501,6 @@ testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { }); return elm; }; + +// test commit 1 + From c011d2c2887c2caecb53e40411b4526d0f0690e9 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 3 Sep 2021 08:54:37 -0400 Subject: [PATCH 03/21] test commit 2 --- test/testutils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/testutils.js b/test/testutils.js index 8c60d13a93..a31429c124 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -501,3 +501,6 @@ testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { }); return elm; }; + +// test commit 2 + From a3861666d61988443343356a8b26760cc3818a6e Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 3 Sep 2021 08:55:07 -0400 Subject: [PATCH 04/21] test commit 3 --- test/testutils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testutils.js b/test/testutils.js index a31429c124..f5b385e6d5 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -502,5 +502,5 @@ testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { return elm; }; -// test commit 2 +// test commit 3 From 31b19a3829bd8d52eacc006af973261e2d4d1f93 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 3 Sep 2021 09:28:17 -0400 Subject: [PATCH 05/21] Revert "Merge branch 'dan-test-branch-1' into develop" This reverts commit 428e01533eb561a5a2ee0a603014057337ba0177, reversing changes made to 9f996bc6f54092541db77b9c3e62e2a5d9d9643e. --- test/testutils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testutils.js b/test/testutils.js index 73d902ed24..5b37f8b83d 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -502,5 +502,5 @@ testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { return elm; }; -// test commit 1 & 3 +// test commit 1 From ac32146f5561fcdafac1c5876e57f3fd8342d8a0 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 3 Sep 2021 09:28:29 -0400 Subject: [PATCH 06/21] Revert "test commit 1" This reverts commit 9f996bc6f54092541db77b9c3e62e2a5d9d9643e. --- test/testutils.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/testutils.js b/test/testutils.js index 5b37f8b83d..8c60d13a93 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -501,6 +501,3 @@ testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { }); return elm; }; - -// test commit 1 - From 30f0e01506979bb92600289bd242ab01d25cb8ec Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Thu, 23 Sep 2021 21:12:17 -0400 Subject: [PATCH 07/21] fix(rule): allow "tabindex=-1" for rules "aria-text" and "nested-interactive" Closes issue #2934 --- .../keyboard/no-focusable-content-evaluate.js | 6 ++++- test/checks/keyboard/no-focusable-content.js | 22 +++++++++++++++++++ .../rules/aria-text/aria-text.html | 6 ++--- .../rules/aria-text/aria-text.json | 4 ++-- .../nested-interactive.html | 12 +++++----- .../nested-interactive.json | 4 +++- .../virtual-rules/nested-interactive.js | 20 +++++++++++++++++ 7 files changed, 62 insertions(+), 12 deletions(-) diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index e45bdfde9f..0f6842405f 100644 --- a/lib/checks/keyboard/no-focusable-content-evaluate.js +++ b/lib/checks/keyboard/no-focusable-content-evaluate.js @@ -1,7 +1,11 @@ import isFocusable from '../../commons/dom/is-focusable'; function focusableDescendants(vNode) { - if (isFocusable(vNode)) { + const isNodeFocusable = isFocusable(vNode); + let tabIndex = parseInt(vNode.attr('tabindex'), 10); + tabIndex = !isNaN(tabIndex) ? tabIndex : null; + + if(tabIndex ? isNodeFocusable && tabIndex >= 0 : isNodeFocusable) { return true; } diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index 1e1a3a8474..9959fda468 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -37,4 +37,26 @@ describe('no-focusable-content tests', function() { ); assert.isFalse(noFocusableContent(null, null, vNode)); }); + + it('should return true on span with tabindex=-1', 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 tabindex=-1', function() { + var vNode = queryFixture(' some text ' + +' ' + +''); + assert.isTrue(noFocusableContent(null, null, vNode)); + }); + + it('should return false on span with tabindex=0', function() { + var vNode = queryFixture(' some text ' + +'anyone is able to focus this ' + +''); + assert.isFalse(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..4059f1e53b 100644 --- a/test/integration/rules/aria-text/aria-text.html +++ b/test/integration/rules/aria-text/aria-text.html @@ -15,8 +15,8 @@

Flattened text because of the explicit role.
-
+
Flattened text because of the - explicit role. + explicit role. Considered non-focusable because of tabindex=-1...
-

+

diff --git a/test/integration/rules/aria-text/aria-text.json b/test/integration/rules/aria-text/aria-text.json index f6c3f1d2d9..3aa00ff23b 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"]], + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] } diff --git a/test/integration/rules/nested-interactive/nested-interactive.html b/test/integration/rules/nested-interactive/nested-interactive.html index beb7fbdf37..0baf57f5bb 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.html +++ b/test/integration/rules/nested-interactive/nested-interactive.html @@ -1,12 +1,14 @@ -
pass
- - - + +
pass
+ + + +
- + diff --git a/test/integration/rules/nested-interactive/nested-interactive.json b/test/integration/rules/nested-interactive/nested-interactive.json index 78d3b43176..9195a46ad1 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.json +++ b/test/integration/rules/nested-interactive/nested-interactive.json @@ -8,6 +8,8 @@ ["#pass3"], ["#pass4"], ["#pass5"], - ["#pass6"] + ["#pass6"], + ["#pass7"], + ["#pass8"] ] } diff --git a/test/integration/virtual-rules/nested-interactive.js b/test/integration/virtual-rules/nested-interactive.js index ba963b99b5..cd364e2b22 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 content with tabindex=-1', 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', From 40025ad09bac49ee56a0d2cd1d45668dc90b54db Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Sat, 27 Nov 2021 20:19:49 -0500 Subject: [PATCH 08/21] work in progress --- .../keyboard/no-focusable-content-evaluate.js | 4 ++- test/checks/keyboard/no-focusable-content.js | 34 +++++++++++++------ .../rules/aria-text/aria-text.html | 14 ++++++-- .../rules/aria-text/aria-text.json | 4 +-- .../nested-interactive.html | 14 ++++---- .../nested-interactive.json | 9 ++--- .../virtual-rules/nested-interactive.js | 8 ++--- 7 files changed, 55 insertions(+), 32 deletions(-) diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index 27824fc4f4..8fc8aa3b34 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 fdea6a0b05..871169f202 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -28,17 +28,22 @@ 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.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); + 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 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 +55,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 +66,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( '' ); @@ -71,25 +76,32 @@ describe('no-focusable-content tests', function() { assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); - it('should return true on span with tabindex=-1', function() { + 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 tabindex=-1', function() { + 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 false on span with tabindex=0', function() { + 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.isFalse(noFocusableContent(null, null, vNode)); + 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 4059f1e53b..54809b6bd5 100644 --- a/test/integration/rules/aria-text/aria-text.html +++ b/test/integration/rules/aria-text/aria-text.html @@ -15,8 +15,16 @@

Flattened text because of the explicit role.
-
+
Flattened text because of the - explicit role. Considered non-focusable because of tabindex=-1... + explicit role. +
+

+

+

Hello

+ + -

diff --git a/test/integration/rules/aria-text/aria-text.json b/test/integration/rules/aria-text/aria-text.json index 3aa00ff23b..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"]], - "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] + "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 a11161f964..1af908b261 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.html +++ b/test/integration/rules/nested-interactive/nested-interactive.html @@ -6,13 +6,13 @@ - -
- - - - - + +
+ + + + + ignored ignored diff --git a/test/integration/rules/nested-interactive/nested-interactive.json b/test/integration/rules/nested-interactive/nested-interactive.json index 7a9c0df654..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"], @@ -18,6 +16,9 @@ ["#pass5"], ["#pass6"], ["#pass7"], - ["#pass8"] + ["#pass8"], + ["#pass9"], + ["#pass10"], + ["#pass11"] ] } diff --git a/test/integration/virtual-rules/nested-interactive.js b/test/integration/virtual-rules/nested-interactive.js index cd364e2b22..fbb94c136b 100644 --- a/test/integration/virtual-rules/nested-interactive.js +++ b/test/integration/virtual-rules/nested-interactive.js @@ -74,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' }); @@ -89,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: { From 92dc637e75d01be40f3ff5c7e79c1789e5341f7c Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Sat, 27 Nov 2021 20:36:54 -0500 Subject: [PATCH 09/21] work in progress --- test/integration/virtual-rules/nested-interactive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/virtual-rules/nested-interactive.js b/test/integration/virtual-rules/nested-interactive.js index fbb94c136b..acdb54f389 100644 --- a/test/integration/virtual-rules/nested-interactive.js +++ b/test/integration/virtual-rules/nested-interactive.js @@ -38,7 +38,7 @@ describe('nested-interactive virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should pass for element with content with tabindex=-1', function() { + it('should pass for element with non-widget content which has negative tabindex', function() { var node = new axe.SerialVirtualNode({ nodeName: 'button' }); From 6d8a0f652b732e59d1c64af430196722369abadd Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 3 Sep 2021 08:48:36 -0400 Subject: [PATCH 10/21] test commit 1 --- test/testutils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/testutils.js b/test/testutils.js index fdc5a8741d..1d86c4f115 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -575,3 +575,6 @@ testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { }); return elm; }; + +// test commit 1 + From 9373c580e4770f8a2cc2572464e0b869239a8e87 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 3 Sep 2021 09:28:29 -0400 Subject: [PATCH 11/21] Revert "test commit 1" This reverts commit 9f996bc6f54092541db77b9c3e62e2a5d9d9643e. --- test/testutils.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/testutils.js b/test/testutils.js index 1d86c4f115..fdc5a8741d 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -575,6 +575,3 @@ testUtils.shadowQuerySelector = function shadowQuerySelector(axeSelector, doc) { }); return elm; }; - -// test commit 1 - From 0a4fd4fa7813f0263971a5d54bd95ec20471d3e2 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Thu, 23 Sep 2021 21:12:17 -0400 Subject: [PATCH 12/21] fix(rule): allow "tabindex=-1" for rules "aria-text" and "nested-interactive" Closes issue #2934 --- test/checks/keyboard/no-focusable-content.js | 22 +++++++++++++++++++ .../rules/aria-text/aria-text.html | 6 ++--- .../rules/aria-text/aria-text.json | 4 ++-- .../nested-interactive.html | 12 +++++----- .../virtual-rules/nested-interactive.js | 20 +++++++++++++++++ 5 files changed, 54 insertions(+), 10 deletions(-) diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index 1389e0241a..fdea6a0b05 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -70,4 +70,26 @@ describe('no-focusable-content tests', function() { assert.deepEqual(checkContext._data, { messageKey: 'notHidden' }); assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); + + it('should return true on span with tabindex=-1', 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 tabindex=-1', function() { + var vNode = queryFixture(' some text ' + +' ' + +''); + assert.isTrue(noFocusableContent(null, null, vNode)); + }); + + it('should return false on span with tabindex=0', function() { + var vNode = queryFixture(' some text ' + +'anyone is able to focus this ' + +''); + assert.isFalse(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..4059f1e53b 100644 --- a/test/integration/rules/aria-text/aria-text.html +++ b/test/integration/rules/aria-text/aria-text.html @@ -15,8 +15,8 @@

Flattened text because of the explicit role.
-
+
Flattened text because of the - explicit role. + explicit role. Considered non-focusable because of tabindex=-1...
-

+

diff --git a/test/integration/rules/aria-text/aria-text.json b/test/integration/rules/aria-text/aria-text.json index f6c3f1d2d9..3aa00ff23b 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"]], + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] } diff --git a/test/integration/rules/nested-interactive/nested-interactive.html b/test/integration/rules/nested-interactive/nested-interactive.html index fb42dd24dc..adec643ff2 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.html +++ b/test/integration/rules/nested-interactive/nested-interactive.html @@ -1,12 +1,14 @@ -
pass
- - - + +
pass
+ + + +
- + diff --git a/test/integration/virtual-rules/nested-interactive.js b/test/integration/virtual-rules/nested-interactive.js index ba963b99b5..cd364e2b22 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 content with tabindex=-1', 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', From d50b56ae3526f12f245218f19902be4bf103294a Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Sat, 27 Nov 2021 20:19:49 -0500 Subject: [PATCH 13/21] work in progress --- .../keyboard/no-focusable-content-evaluate.js | 4 ++- test/checks/keyboard/no-focusable-content.js | 34 +++++++++++++------ .../rules/aria-text/aria-text.html | 14 ++++++-- .../rules/aria-text/aria-text.json | 4 +-- .../nested-interactive.html | 14 ++++---- .../nested-interactive.json | 10 +++--- .../virtual-rules/nested-interactive.js | 8 ++--- 7 files changed, 56 insertions(+), 32 deletions(-) diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index 27824fc4f4..8fc8aa3b34 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 fdea6a0b05..871169f202 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -28,17 +28,22 @@ 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.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); + 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 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 +55,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 +66,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( '' ); @@ -71,25 +76,32 @@ describe('no-focusable-content tests', function() { assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); }); - it('should return true on span with tabindex=-1', function() { + 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 tabindex=-1', function() { + 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 false on span with tabindex=0', function() { + 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.isFalse(noFocusableContent(null, null, vNode)); + 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 4059f1e53b..54809b6bd5 100644 --- a/test/integration/rules/aria-text/aria-text.html +++ b/test/integration/rules/aria-text/aria-text.html @@ -15,8 +15,16 @@

Flattened text because of the explicit role.
-
+
Flattened text because of the - explicit role. Considered non-focusable because of tabindex=-1... + explicit role. +
+

+

+

Hello

+ + -

diff --git a/test/integration/rules/aria-text/aria-text.json b/test/integration/rules/aria-text/aria-text.json index 3aa00ff23b..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"]], - "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"]] + "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 adec643ff2..1af908b261 100644 --- a/test/integration/rules/nested-interactive/nested-interactive.html +++ b/test/integration/rules/nested-interactive/nested-interactive.html @@ -6,13 +6,13 @@ - -
- - - - - + +
+ + + + + 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 cd364e2b22..fbb94c136b 100644 --- a/test/integration/virtual-rules/nested-interactive.js +++ b/test/integration/virtual-rules/nested-interactive.js @@ -74,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' }); @@ -89,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: { From 74d5656c1e9e7b21a16fa226d1f5c8ec407d4d4b Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Sat, 27 Nov 2021 20:36:54 -0500 Subject: [PATCH 14/21] work in progress --- test/integration/virtual-rules/nested-interactive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/virtual-rules/nested-interactive.js b/test/integration/virtual-rules/nested-interactive.js index fbb94c136b..acdb54f389 100644 --- a/test/integration/virtual-rules/nested-interactive.js +++ b/test/integration/virtual-rules/nested-interactive.js @@ -38,7 +38,7 @@ describe('nested-interactive virtual-rule', function() { assert.lengthOf(results.incomplete, 0); }); - it('should pass for element with content with tabindex=-1', function() { + it('should pass for element with non-widget content which has negative tabindex', function() { var node = new axe.SerialVirtualNode({ nodeName: 'button' }); From 54329dd5048656f4c1f81c200a9027d783cad3a2 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Mon, 29 Nov 2021 20:42:32 -0500 Subject: [PATCH 15/21] fix whitespace --- lib/checks/keyboard/no-focusable-content-evaluate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/checks/keyboard/no-focusable-content-evaluate.js b/lib/checks/keyboard/no-focusable-content-evaluate.js index 8fc8aa3b34..f72bad79d7 100644 --- a/lib/checks/keyboard/no-focusable-content-evaluate.js +++ b/lib/checks/keyboard/no-focusable-content-evaluate.js @@ -42,7 +42,7 @@ function getFocusableDescendants(vNode) { const retVal = []; vNode.children.forEach(child => { const role = getRole(child); - if(getRoleType(role) === 'widget' && isFocusable(child)) { + if (getRoleType(role) === 'widget' && isFocusable(child)) { retVal.push(child); } else { retVal.push(...getFocusableDescendants(child)); From 499768f95466e6545a0410dda5a342fd576e25a6 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Mon, 29 Nov 2021 20:54:09 -0500 Subject: [PATCH 16/21] add new case to test test/checks/keyboard/no-focusable-content.js --- test/checks/keyboard/no-focusable-content.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index 871169f202..68137b433b 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -43,6 +43,16 @@ describe('no-focusable-content tests', function() { 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 content which is natively focusable and has a widget role', function() { var params = checkSetup( '' From 8c32f0306e866acfff167d8183e1091321732eb7 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Mon, 29 Nov 2021 20:55:50 -0500 Subject: [PATCH 17/21] change "disabled" test case in test/checks/keyboard/no-focusable-content.js --- test/checks/keyboard/no-focusable-content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index 68137b433b..d642bbd15e 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -88,7 +88,7 @@ describe('no-focusable-content tests', function() { 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)); }); From d6a5573af6c5d23c4ec906db2536f8679e8de8c0 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Sun, 5 Dec 2021 11:51:02 -0500 Subject: [PATCH 18/21] fix merge problem --- test/checks/keyboard/no-focusable-content.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index 79d847d31c..d642bbd15e 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -41,7 +41,6 @@ describe('no-focusable-content tests', function() { '' ); assert.isTrue(noFocusableContent(null, null, vNode)); -<<<<<<< HEAD }); it('should return false if element has content which has negative tabindex and an explicit widget role', function() { @@ -52,8 +51,6 @@ describe('no-focusable-content tests', function() { assert.isFalse(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { messageKey: 'notHidden' }); assert.deepEqual(checkContext._relatedNodes, [params[2].children[0]]); -======= ->>>>>>> 7e692b4a67fec1f93e362d627b94ca94980bf4f6 }); it('should return false if element has content which is natively focusable and has a widget role', function() { From 593aaac5ffee3b56b31e30e28d4fe2799defcead Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Fri, 11 Feb 2022 19:50:06 -0500 Subject: [PATCH 19/21] fix(commons/dom): focusDisabled() behavior Now checks "disabled" attribute Closes issue #3315 --- lib/commons/dom/focus-disabled.js | 9 ++++++++- test/checks/keyboard/no-focusable-content.js | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/commons/dom/focus-disabled.js b/lib/commons/dom/focus-disabled.js index e1288f1be8..1153d01518 100644 --- a/lib/commons/dom/focus-disabled.js +++ b/lib/commons/dom/focus-disabled.js @@ -2,6 +2,13 @@ import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-n import { getNodeFromTree } from '../../core/utils'; import isHiddenWithCSS from './is-hidden-with-css'; +function isDisabledAttrAllowed(nodeName) { + // Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled + const allowedNodeNames = new Set(["button", "command", "fieldset", "keygen", "optgroup", + "option", "select", "textarea", "input"]); + return allowedNodeNames.has(nodeName); +} + /** * Determines if focusing has been disabled on an element. * @param {HTMLElement|VirtualNode} el The HTMLElement @@ -10,7 +17,7 @@ import isHiddenWithCSS from './is-hidden-with-css'; function focusDisabled(el) { const vNode = el instanceof AbstractVirtualNode ? el : getNodeFromTree(el); - if (vNode.hasAttr('disabled')) { + if (isDisabledAttrAllowed(vNode.props.nodeName) && vNode.hasAttr('disabled')) { return true; } diff --git a/test/checks/keyboard/no-focusable-content.js b/test/checks/keyboard/no-focusable-content.js index d642bbd15e..e10b4679b0 100644 --- a/test/checks/keyboard/no-focusable-content.js +++ b/test/checks/keyboard/no-focusable-content.js @@ -93,6 +93,13 @@ describe('no-focusable-content tests', function() { assert.isTrue(noFocusableContent(null, null, vNode)); }); + it('should return false if "disabled" is specified on an element which doesn\'t allow it', function() { + var params = checkSetup( + '' + ); + assert.isFalse(noFocusableContent.apply(checkContext, params)); + }); + 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 ' From 903fbd597e4276da915752c695dff994847718cd Mon Sep 17 00:00:00 2001 From: Dan Tripp <88439449+dan-tripp@users.noreply.github.com> Date: Mon, 21 Feb 2022 14:05:40 -0500 Subject: [PATCH 20/21] Update lib/commons/dom/focus-disabled.js Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com> --- lib/commons/dom/focus-disabled.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/commons/dom/focus-disabled.js b/lib/commons/dom/focus-disabled.js index 1153d01518..0bf2e737f8 100644 --- a/lib/commons/dom/focus-disabled.js +++ b/lib/commons/dom/focus-disabled.js @@ -1,12 +1,22 @@ import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; import { getNodeFromTree } from '../../core/utils'; import isHiddenWithCSS from './is-hidden-with-css'; +// Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled +const allowedDisabledNodeNames = [ + 'button', + 'command', + 'fieldset', + 'keygen', + 'optgroup', + 'option', + 'select', + 'textarea', + 'input' +]; function isDisabledAttrAllowed(nodeName) { - // Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled - const allowedNodeNames = new Set(["button", "command", "fieldset", "keygen", "optgroup", - "option", "select", "textarea", "input"]); - return allowedNodeNames.has(nodeName); + return allowedDisabledNodeNames.includes(nodeName); +} } /** From 404a09224f921b3125b6b50b1ad0a1edbcdd6dd1 Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Mon, 21 Feb 2022 14:14:02 -0500 Subject: [PATCH 21/21] fix typo --- lib/commons/dom/focus-disabled.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/commons/dom/focus-disabled.js b/lib/commons/dom/focus-disabled.js index 0bf2e737f8..108ec7b301 100644 --- a/lib/commons/dom/focus-disabled.js +++ b/lib/commons/dom/focus-disabled.js @@ -17,7 +17,6 @@ const allowedDisabledNodeNames = [ function isDisabledAttrAllowed(nodeName) { return allowedDisabledNodeNames.includes(nodeName); } -} /** * Determines if focusing has been disabled on an element.