diff --git a/lib/checks/aria/valid-attr-value.js b/lib/checks/aria/valid-attr-value.js index 548d52e805..e007aae363 100644 --- a/lib/checks/aria/valid-attr-value.js +++ b/lib/checks/aria/valid-attr-value.js @@ -1,26 +1,35 @@ options = Array.isArray(options) ? options : []; -var invalid = [], - aria = /^aria-/; +const invalid = []; +const aria = /^aria-/; +const attrs = axe.utils.getNodeAttributes(node); -var attr, - attrName, - attrs = axe.utils.getNodeAttributes(node); +const skipAttrs = ['aria-errormessage']; -var skipAttrs = ['aria-errormessage']; +// aria-controls should only check if element exists if the element +// doesn't have aria-expanded=false or aria-selected=false (tabs) +// @see https://github.com/dequelabs/axe-core/issues/1463 +const preChecks = { + 'aria-controls': function() { + return ( + node.getAttribute('aria-expanded') !== 'false' && + node.getAttribute('aria-selected') !== 'false' + ); + } +}; -for (var i = 0, l = attrs.length; i < l; i++) { - attr = attrs[i]; - attrName = attr.name; +for (let i = 0, l = attrs.length; i < l; i++) { + const attr = attrs[i]; + const attrName = attr.name; // skip any attributes handled elsewhere - if (!skipAttrs.includes(attrName)) { - if ( - options.indexOf(attrName) === -1 && - aria.test(attrName) && - !axe.commons.aria.validateAttrValue(node, attrName) - ) { - invalid.push(attrName + '="' + attr.nodeValue + '"'); - } + if ( + !skipAttrs.includes(attrName) && + options.indexOf(attrName) === -1 && + aria.test(attrName) && + (preChecks[attrName] ? preChecks[attrName]() : true) && + !axe.commons.aria.validateAttrValue(node, attrName) + ) { + invalid.push(`${attrName}="${attr.nodeValue}"`); } } diff --git a/test/checks/aria/valid-attr-value.js b/test/checks/aria/valid-attr-value.js index aaef387a5f..b7f4109335 100644 --- a/test/checks/aria/valid-attr-value.js +++ b/test/checks/aria/valid-attr-value.js @@ -129,6 +129,54 @@ describe('aria-valid-attr-value', function() { ); }); + it('should pass on aria-controls and aria-expanded=false when the element is not in the DOM', function() { + fixtureSetup( + '' + ); + var passing1 = fixture.querySelector('button'); + assert.isTrue( + checks['aria-valid-attr-value'].evaluate.call(checkContext, passing1) + ); + }); + + it('should pass on aria-controls and aria-selected=false when the element is not in the DOM', function() { + fixtureSetup( + '' + ); + var passing1 = fixture.querySelector('button'); + assert.isTrue( + checks['aria-valid-attr-value'].evaluate.call(checkContext, passing1) + ); + }); + + it('should fail on aria-controls and aria-expanded=true when the element is not in the DOM', function() { + fixtureSetup( + '' + ); + var failing1 = fixture.querySelector('button'); + assert.isFalse( + checks['aria-valid-attr-value'].evaluate.call(checkContext, failing1) + ); + }); + + it('should fail on aria-controls and aria-selected=true when the element is not in the DOM', function() { + fixtureSetup( + '' + ); + var failing1 = fixture.querySelector('button'); + assert.isFalse( + checks['aria-valid-attr-value'].evaluate.call(checkContext, failing1) + ); + }); + + it('should fail on aria-controls when the element is not in the DOM', function() { + fixtureSetup(''); + var failing1 = fixture.querySelector('button'); + assert.isFalse( + checks['aria-valid-attr-value'].evaluate.call(checkContext, failing1) + ); + }); + describe('options', function() { it('should exclude supplied attributes', function() { fixture.innerHTML =