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 =