diff --git a/lib/commons/aria/get-element-unallowed-roles.js b/lib/commons/aria/get-element-unallowed-roles.js index 2fbb6dd337..b371dee71a 100644 --- a/lib/commons/aria/get-element-unallowed-roles.js +++ b/lib/commons/aria/get-element-unallowed-roles.js @@ -1,4 +1,46 @@ /* global aria */ + +/** + * Returns all roles applicable to element in a list + * + * @method getRoleSegments + * @private + * @param {Element} node + * @returns {Array} Roles list or empty list + */ + +function getRoleSegments(node) { + let roles = []; + + if (!node) { + return roles; + } + + if (node.hasAttribute('role')) { + const nodeRoles = axe.utils.tokenList( + node.getAttribute('role').toLowerCase() + ); + roles = roles.concat(nodeRoles); + } + + if (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type')) { + const epubRoles = axe.utils + .tokenList( + node + .getAttributeNS('http://www.idpf.org/2007/ops', 'type') + .toLowerCase() + ) + .map(role => `doc-${role}`); + + roles = roles.concat(epubRoles); + } + + // filter invalid roles + roles = roles.filter(role => axe.commons.aria.isValidRole(role)); + + return roles; +} + /** * gets all unallowed roles for a given node * @method getElementUnallowedRoles @@ -9,38 +51,8 @@ */ aria.getElementUnallowedRoles = function getElementUnallowedRoles( node, - allowImplicit + allowImplicit = true ) { - /** - * Get roles applied to a given node - * @param {HTMLElement} node HTMLElement - * @return {Array} return an array of roles applied to the node, if no roles, return an empty array. - */ - // TODO: not moving this to outer namespace yet, work with wilco to see overlap with his PR(WIP) - aria.getRole - function getRoleSegments(node) { - let roles = []; - if (!node) { - return roles; - } - if (node.hasAttribute('role')) { - const nodeRoles = axe.utils.tokenList( - node.getAttribute('role').toLowerCase() - ); - roles = roles.concat(nodeRoles); - } - if (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type')) { - const epubRoles = axe.utils - .tokenList( - node - .getAttributeNS('http://www.idpf.org/2007/ops', 'type') - .toLowerCase() - ) - .map(role => `doc-${role}`); - roles = roles.concat(epubRoles); - } - return roles; - } - const tagName = node.nodeName.toUpperCase(); // by pass custom elements @@ -53,24 +65,26 @@ aria.getElementUnallowedRoles = function getElementUnallowedRoles( // stores all roles that are not allowed for a specific element most often an element only has one explicit role const unallowedRoles = roleSegments.filter(role => { - if (!axe.commons.aria.isValidRole(role)) { - // do not check made-up/ fake roles + // if role and implicit role are same, when allowImplicit: true + // ignore as it is a redundant role + if (allowImplicit && role === implicitRole) { return false; } - // check if an implicit role may be set explicit following a setting - if (!allowImplicit && role === implicitRole) { - // edge case: setting implicit role row on tr element is allowed when child of table[role='grid'] - if ( - !( - role === 'row' && - tagName === 'TR' && - axe.utils.matchesSelector(node, 'table[role="grid"] > tr') - ) - ) { - return true; - } + // Edge case: + // setting implicit role row on tr element is allowed when child of table[role='grid'] + if ( + !allowImplicit && + !( + role === 'row' && + tagName === 'TR' && + axe.utils.matchesSelector(node, 'table[role="grid"] > tr') + ) + ) { + return true; } + + // check if role is allowed on element if (!aria.isAriaRoleAllowedOnElement(node, role)) { return true; } diff --git a/lib/commons/aria/get-role.js b/lib/commons/aria/get-role.js index ae9aa3ce3b..1ab0bae5d8 100644 --- a/lib/commons/aria/get-role.js +++ b/lib/commons/aria/get-role.js @@ -28,6 +28,7 @@ aria.getRole = function getRole( } return aria.isValidRole(role, { allowAbstract: abstracts }); }); + const explicitRole = validRoles[0]; // Get the implicit role, if permitted diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js index d5cd6580b2..90abf48c8f 100644 --- a/lib/commons/aria/index.js +++ b/lib/commons/aria/index.js @@ -420,7 +420,15 @@ lookupTable.role = { }, nameFrom: ['author'], context: null, - unsupported: false + unsupported: false, + allowedElements: [ + { + tagName: 'INPUT', + attributes: { + TYPE: 'TEXT' + } + } + ] }, command: { nameFrom: ['author'], @@ -1687,7 +1695,15 @@ lookupTable.role = { nameFrom: ['author'], context: null, implicit: ['input[type="search"]'], - unsupported: false + unsupported: false, + allowedElements: [ + { + tagName: 'INPUT', + attributes: { + TYPE: 'TEXT' + } + } + ] }, section: { nameFrom: ['author', 'contents'], @@ -1756,7 +1772,15 @@ lookupTable.role = { nameFrom: ['author'], context: null, implicit: ['input[type="number"]'], - unsupported: false + unsupported: false, + allowedElements: [ + { + tagName: 'INPUT', + attributes: { + TYPE: 'TEXT' + } + } + ] }, status: { type: 'widget', @@ -2148,13 +2172,6 @@ lookupTable.elementsAllowedNoRole = [ TYPE: 'TEL' } }, - { - tagName: 'INPUT', - condition: elementConditions.CANNOT_HAVE_LIST_ATTRIBUTE, - attributes: { - TYPE: 'TEXT' - } - }, { tagName: 'INPUT', attributes: { @@ -2368,6 +2385,10 @@ lookupTable.evaluateRoleForElement = { return out; case 'radio': return role === 'menuitemradio'; + case 'text': + return ( + role === 'combobox' || role === 'searchbox' || role === 'spinbutton' + ); default: return false; } diff --git a/lib/rules/aria-allowed-role-matches.js b/lib/rules/aria-allowed-role-matches.js new file mode 100644 index 0000000000..b3375967a3 --- /dev/null +++ b/lib/rules/aria-allowed-role-matches.js @@ -0,0 +1,7 @@ +return ( + axe.commons.aria.getRole(node, { + noImplicit: true, + dpub: true, + fallback: true + }) !== null +); diff --git a/lib/rules/aria-allowed-role.json b/lib/rules/aria-allowed-role.json index ad71e25d1e..3c0c8941a0 100644 --- a/lib/rules/aria-allowed-role.json +++ b/lib/rules/aria-allowed-role.json @@ -2,6 +2,7 @@ "id": "aria-allowed-role", "excludeHidden": false, "selector": "[role]", + "matches": "aria-allowed-role-matches.js", "tags": [ "cat.aria", "best-practice" diff --git a/test/checks/aria/aria-allowed-role.js b/test/checks/aria/aria-allowed-role.js index 181266f3a6..d3a56ef325 100644 --- a/test/checks/aria/aria-allowed-role.js +++ b/test/checks/aria/aria-allowed-role.js @@ -91,7 +91,37 @@ describe('aria-allowed-role', function() { ); }); - it('returns false when MENU has type context', function() { + it('returns true when INPUT type is text with role combobox', function() { + var node = document.createElement('input'); + node.setAttribute('type', 'text'); + node.setAttribute('role', 'combobox'); + fixture.appendChild(node); + assert.isTrue( + checks['aria-allowed-role'].evaluate.call(checkContext, node) + ); + }); + + it('returns true when INPUT type is text with role spinbutton', function() { + var node = document.createElement('input'); + node.setAttribute('type', 'text'); + node.setAttribute('role', 'spinbutton'); + fixture.appendChild(node); + assert.isTrue( + checks['aria-allowed-role'].evaluate.call(checkContext, node) + ); + }); + + it('returns true when INPUT type is text with role searchbox', function() { + var node = document.createElement('input'); + node.setAttribute('type', 'text'); + node.setAttribute('role', 'searchbox'); + fixture.appendChild(node); + assert.isTrue( + checks['aria-allowed-role'].evaluate.call(checkContext, node) + ); + }); + + it('returns true when MENU has type context', function() { var node = document.createElement('menu'); node.setAttribute('type', 'context'); node.setAttribute('role', 'navigation'); @@ -135,10 +165,8 @@ describe('aria-allowed-role', function() { node.setAttribute('role', 'link'); node.href = ''; fixture.appendChild(node); - assert.isFalse( - checks['aria-allowed-role'].evaluate.call(checkContext, node) - ); - assert.deepEqual(checkContext._data, ['link']); + var actual = checks['aria-allowed-role'].evaluate.call(checkContext, node); + assert.isTrue(actual); }); it('returns true with a non-empty alt', function() { @@ -159,17 +187,6 @@ describe('aria-allowed-role', function() { assert.isFalse( checks['aria-allowed-role'].evaluate.call(checkContext, node) ); - // assert.deepEqual(checkContext._data, ['presentation']); - }); - - it('should not allow a with a href to have any invalid role', function() { - var node = document.createElement('link'); - node.setAttribute('role', 'invalid-role'); - node.href = '\\example.com'; - fixture.appendChild(node); - assert.isTrue( - checks['aria-allowed-role'].evaluate.call(checkContext, node) - ); }); it('should allow + + + + +

@@ -46,4 +52,6 @@

- \ No newline at end of file + + + diff --git a/test/integration/rules/aria-allowed-role/aria-allowed-role.json b/test/integration/rules/aria-allowed-role/aria-allowed-role.json index a18890b583..a92c112926 100644 --- a/test/integration/rules/aria-allowed-role/aria-allowed-role.json +++ b/test/integration/rules/aria-allowed-role/aria-allowed-role.json @@ -11,7 +11,6 @@ ["#pass-section-role-doc-bib"], ["#pass-li-role-doc-biblioentry"], ["#pass-aside-doc-example"], - ["#pass-div-has-any-role"], ["#pass-div-valid-role"], ["#pass-ol-valid-role"], ["#pass-nav-role-doc-index"], @@ -31,6 +30,13 @@ ["#pass-header-valid-role"], ["#pass-footer-valid-role"], ["#pass-embed-valid-role"], + ["#pass-div-has-any-role"], + ["#pass-input-text-redundant-role"], + ["#pass-input-multiple-roles"], + ["#pass-input-multiple-valid-and-invalid-roles"], + ["#pass-input-text-role-searchbox"], + ["#pass-input-text-role-combobox"], + ["#pass-input-text-role-spinbutton"], ["#pass-input-image-valid-role"], ["#pass-input-checkbox-valid-role"], ["#pass-h1-valid-role"], @@ -52,6 +58,8 @@ ["#fail-input-image-invalid-role"], ["#fail-button-role-cell"], ["#fail-aside-doc-foreword"], - ["#fail-aside-role-tab"] + ["#fail-aside-role-tab"], + ["#fail-button-role-gridcell"], + ["#fail-input-role-gridcell-multiple-role"] ] } \ No newline at end of file diff --git a/test/rule-matches/aria-allowed-role-matches.js b/test/rule-matches/aria-allowed-role-matches.js new file mode 100644 index 0000000000..cb8d383122 --- /dev/null +++ b/test/rule-matches/aria-allowed-role-matches.js @@ -0,0 +1,45 @@ +describe('aria-allowed-role-matches', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var rule; + + beforeEach(function() { + rule = axe._audit.rules.find(function(rule) { + return rule.id === 'aria-allowed-role'; + }); + }); + + afterEach(function() { + fixture.innerHTML = ''; + }); + + it('is a function', function() { + assert.isFunction(rule.matches); + }); + + it('return false (no matches) for a with a href to have any invalid role', function() { + var node = document.createElement('link'); + node.setAttribute('role', 'invalid-role'); + node.href = '\\example.com'; + fixture.appendChild(node); + assert.isFalse(rule.matches(node)); + }); + + it('return true for input with redundant role', function() { + var node = document.createElement('input'); + node.setAttribute('type', 'text'); + node.setAttribute('role', 'textbox'); + node.href = '\\example.com'; + fixture.appendChild(node); + assert.isTrue(rule.matches(node)); + }); + + it('return true for element with valid role', function() { + var node = document.createElement('ol'); + node.setAttribute('role', 'listbox'); + node.href = '\\example.com'; + fixture.appendChild(node); + assert.isTrue(rule.matches(node)); + }); +});