diff --git a/lib/checks/aria/errormessage.js b/lib/checks/aria/errormessage.js new file mode 100644 index 0000000000..0744ad198e --- /dev/null +++ b/lib/checks/aria/errormessage.js @@ -0,0 +1,25 @@ +options = Array.isArray(options) ? options : []; + +var attr = node.getAttribute('aria-errormessage'), + hasAttr = node.hasAttribute('aria-errormessage'); + +var doc = axe.commons.dom.getRootNode(node); + +function validateAttrValue() { + var idref = attr && doc.getElementById(attr); + if (idref) { + return idref.getAttribute('role') === 'alert' || + idref.getAttribute('aria-live') === 'assertive' || + axe.utils.tokenList(node.getAttribute('aria-describedby') || '').indexOf(attr) > -1; + } +} + +// limit results to elements that actually have this attribute +if (options.indexOf(attr) === -1 && hasAttr) { + if (!validateAttrValue()) { + this.data(attr); + return false; + } + + return true; +} diff --git a/lib/checks/aria/errormessage.json b/lib/checks/aria/errormessage.json new file mode 100644 index 0000000000..5bf3563ce3 --- /dev/null +++ b/lib/checks/aria/errormessage.json @@ -0,0 +1,11 @@ +{ + "id": "aria-errormessage", + "evaluate": "errormessage.js", + "metadata": { + "impact": "critical", + "messages": { + "pass": "Uses a supported aria-errormessage technique", + "fail": "aria-errormessage value{{=it.data && it.data.length > 1 ? 's' : ''}} {{~it.data:value}} `{{=value}}{{~}}` must use a technique to announce the message (e.g., aria-live, aria-describedby, role=alert, etc.)" + } + } +} diff --git a/lib/checks/aria/valid-attr-value.js b/lib/checks/aria/valid-attr-value.js index 02ffad9ac0..b5a7c58757 100644 --- a/lib/checks/aria/valid-attr-value.js +++ b/lib/checks/aria/valid-attr-value.js @@ -6,13 +6,18 @@ var invalid = [], var attr, attrName, attrs = node.attributes; +var skipAttrs = ['aria-errormessage']; + for (var i = 0, l = attrs.length; i < l; i++) { attr = attrs[i]; attrName = attr.name; - if (options.indexOf(attrName) === -1 && aria.test(attrName) && - !axe.commons.aria.validateAttrValue(node, attrName)) { + // 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 + '"'); + invalid.push(attrName + '="' + attr.nodeValue + '"'); + } } } diff --git a/lib/commons/aria/attributes.js b/lib/commons/aria/attributes.js index baaf81a838..6a34d6555d 100644 --- a/lib/commons/aria/attributes.js +++ b/lib/commons/aria/attributes.js @@ -36,24 +36,6 @@ aria.validateAttr = function (att) { return !!lookupTables.attributes[att]; }; -/** - * Validate the value of an ARIA attribute with an idref - * @param {HTMLElement} doc Root element - * @param {HTMLElement} node The element to check - * @param {String} attr The name of the attribute - * @param {String} value The value of the attribute - * @return {Boolean} - */ -aria.validateIdrefType = function (doc, node, attr, value) { - var idref = value && doc.getElementById(value); - if (idref && attr === 'aria-errormessage') { - return idref.getAttribute('role') === 'alert' || - idref.getAttribute('aria-live') === 'assertive' || - axe.utils.tokenList(node.getAttribute('aria-describedby') || '').indexOf(value) > -1; - } - return !!idref; -}; - /** * Validate the value of an ARIA attribute * @param {HTMLElement} node The element to check @@ -87,7 +69,7 @@ aria.validateAttrValue = function (node, attr) { }, list.length !== 0); case 'idref': - return aria.validateIdrefType(doc, node, attr, value); + return !!(value && doc.getElementById(value)); case 'idrefs': list = axe.utils.tokenList(value); diff --git a/lib/rules/aria-valid-attr-value.json b/lib/rules/aria-valid-attr-value.json index b009bb5914..0caed21830 100644 --- a/lib/rules/aria-valid-attr-value.json +++ b/lib/rules/aria-valid-attr-value.json @@ -14,6 +14,7 @@ }, "all": [], "any": [ + "aria-errormessage", "aria-valid-attr-value" ], "none": [] diff --git a/test/checks/aria/errormessage.js b/test/checks/aria/errormessage.js new file mode 100644 index 0000000000..819cd875b8 --- /dev/null +++ b/test/checks/aria/errormessage.js @@ -0,0 +1,49 @@ +describe('aria-errormessage', function () { + 'use strict'; + + var fixture = document.getElementById('fixture'); + + var checkContext = axe.testUtils.MockCheckContext(); + + afterEach(function () { + fixture.innerHTML = ''; + checkContext._data = null; + }); + + it('should return false if aria-errormessage value is invalid', function () { + var testHTML = '
'; + testHTML += '
'; + fixture.innerHTML = testHTML; + var target = fixture.children[0]; + target.setAttribute('aria-errormessage', 'plain'); + assert.isFalse(checks['aria-errormessage'].evaluate.call(checkContext, target)); + }); + + it('should return true if aria-errormessage id is alert', function () { + var testHTML = '
'; + testHTML += ''; + fixture.innerHTML = testHTML; + var target = fixture.children[0]; + target.setAttribute('aria-errormessage', 'alert'); + assert.isTrue(checks['aria-errormessage'].evaluate.call(checkContext, target)); + }); + + it('should return true if aria-errormessage id is aria-live=assertive', function () { + var testHTML = '
'; + testHTML += '
'; + fixture.innerHTML = testHTML; + var target = fixture.children[0]; + target.setAttribute('aria-errormessage', 'live'); + assert.isTrue(checks['aria-errormessage'].evaluate.call(checkContext, target)); + }); + + it('should return true if aria-errormessage id is aria-describedby', function () { + var testHTML = '
'; + testHTML += '
'; + fixture.innerHTML = testHTML; + var target = fixture.children[0]; + target.setAttribute('aria-errormessage', 'plain'); + target.setAttribute('aria-describedby', 'plain'); + assert.isTrue(checks['aria-errormessage'].evaluate.call(checkContext, target)); + }); +}); diff --git a/test/checks/aria/valid-attr-value.js b/test/checks/aria/valid-attr-value.js index 0f415536c4..a375600de4 100644 --- a/test/checks/aria/valid-attr-value.js +++ b/test/checks/aria/valid-attr-value.js @@ -60,23 +60,6 @@ describe('aria-valid-attr-value', function () { assert.isNull(checkContext._data); }); - it('should return true if aria-errormessage id is alert or aria-describedby', function () { - var testHTML = '
'; - testHTML += '
'; - testHTML += '
'; - testHTML += ''; - fixture.innerHTML = testHTML; - var target = fixture.children[0]; - target.setAttribute('aria-errormessage', 'plain'); - assert.isFalse(checks['aria-valid-attr-value'].evaluate.call(checkContext, target)); - target.setAttribute('aria-errormessage', 'live'); - assert.isTrue(checks['aria-valid-attr-value'].evaluate.call(checkContext, target)); - target.setAttribute('aria-errormessage', 'alert'); - assert.isTrue(checks['aria-valid-attr-value'].evaluate.call(checkContext, target)); - target.setAttribute('aria-describedby', 'plain'); - assert.isTrue(checks['aria-valid-attr-value'].evaluate.call(checkContext, target)); - }); - it('should return false if any values are invalid', function () { var node = document.createElement('div'); node.id = 'test';