-
Notifications
You must be signed in to change notification settings - Fork 780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: ignore invalid and allow redundant role in aria-allowed-role #1118
Changes from 5 commits
119fe33
fceb0d7
4ea825a
401d41e
2086d07
dcacb51
2170563
39e7784
a54843e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,11 +12,12 @@ | |
* @param {boolean} options.fallback Allow fallback roles | ||
* @param {boolean} options.abstracts Allow role to be abstract | ||
* @param {boolean} options.dpub Allow role to be any (valid) doc-* roles | ||
* @param {boolean} options.segments Allows getting full list of roles applied | ||
* @returns {string|null} Role or null | ||
*/ | ||
aria.getRole = function getRole( | ||
node, | ||
{ noImplicit, fallback, abstracts, dpub } = {} | ||
{ noImplicit, fallback, abstracts, dpub, segments } = {} | ||
) { | ||
const roleAttr = (node.getAttribute('role') || '').trim().toLowerCase(); | ||
const roleList = fallback ? axe.utils.tokenList(roleAttr) : [roleAttr]; | ||
|
@@ -28,6 +29,11 @@ aria.getRole = function getRole( | |
} | ||
return aria.isValidRole(role, { allowAbstract: abstracts }); | ||
}); | ||
|
||
if (segments) { | ||
return validRoles; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not at all obvious that |
||
|
||
const explicitRole = validRoles[0]; | ||
|
||
// Get the implicit role, if permitted | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General question: How did we miss this, and how do we know we're not missing more stuff this time around? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the holy grail source of truth - https://www.w3.org/TR/html-aria/ to construct these mappings. There are 3 caveats with this:
To answer your specific Q, we missed this case due to an overlap of 1 and 2 above. I am going through the table once more to ensure the mappings are right. |
||
{ | ||
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; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
const { isValidRole, getRole } = axe.commons.aria; | ||
|
||
// get role(s) as array(segments), with fallback and dpub | ||
const roleSegments = getRole(node, { | ||
segments: true, | ||
fallback: true, | ||
dpub: true | ||
}); | ||
|
||
// filter invalid roles | ||
const validRoles = roleSegments.filter(role => isValidRole(role)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getRole already filters out invalid roles, doesn't it? Couldn't you just do this: return getRole(node, { noImplicit: true, dpub: true }) !== null |
||
|
||
// if no valid roles are applied, ignore | ||
if (!validRoles.length) { | ||
return false; | ||
} | ||
|
||
return true; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,6 +91,36 @@ describe('aria-allowed-role', 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 false when MENU has type context', function() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the test description needs to be inverted to match the new expected result. |
||
var node = document.createElement('menu'); | ||
node.setAttribute('type', 'context'); | ||
|
@@ -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 <img> 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 <link> 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 <select> without a multiple and size attribute to have a menu role', function() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,6 @@ | |
<section id="pass-section-role-doc-bib" role="doc-bibliography"></section> | ||
<ul><li id='pass-li-role-doc-biblioentry' role="doc-biblioentry"></li></ul> | ||
<aside id='pass-aside-doc-example' role='doc-example'></aside> | ||
<div id='pass-div-has-any-role' role='divAnyRoleEvenInvalid'></div> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of removing, I suggest you put in a valid role. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. Added it back again. This diff only watches this line, so comment is not going stale. |
||
<div id="pass-div-valid-role" role="link">ok</div> | ||
<ol id="pass-ol-valid-role" role="directory"></ol> | ||
<nav id="pass-nav-role-doc-index" role="doc-index"></nav> | ||
|
@@ -27,6 +26,10 @@ <h1 id="pass-h1-role-doc-subtitle" role="doc-subtitle"></h1> | |
<header id='pass-header-valid-role' role="group"></header> | ||
<footer id='pass-footer-valid-role' role="group"></footer> | ||
<embed id='pass-embed-valid-role' role='img'> | ||
<input type="text" role="textbox" id="pass-input-text-redundant-role"/> | ||
<input aria-autocomplete="list" aria-label="some label" autocomplete="off" role="searchbox" type="text" aria-expanded="true" aria-owns="autocomplete-0" id='pass-input-text-role-searchbox'> | ||
<input aria-autocomplete="list" aria-label="some label" autocomplete="off" role="combobox" type="text" aria-expanded="true" aria-owns="autocomplete-0" id='pass-input-text-role-combobox'> | ||
<input aria-autocomplete="list" aria-label="some label" autocomplete="off" role="spinbutton" type="text" aria-expanded="true" aria-owns="autocomplete-0" id='pass-input-text-role-spinbutton'> | ||
<input type="image" role="link" id="pass-input-image-valid-role"> | ||
<input type="checkbox" role='menuitemcheckbox' id='pass-input-checkbox-valid-role' > | ||
<h1 id='pass-h1-valid-role' role='none'></h1> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <link> 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)); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused about why you're doing this for a second time. Line 28 already tests for implicit roles, correctly taking the allowImplicit option into consideration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, that flow path, only handles the edge case for
table>tr[role='row']
. I shall refactor.