Skip to content
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(aria-allowed-role): landmark roles banner on header and contentinfo on footer to only report on top-level rule #3142

Merged
merged 27 commits into from
Nov 24, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
23a1d92
Added banner role to header element
Zidious Aug 31, 2021
94c707c
add test to check for ########## …
Zidious Sep 3, 2021
f4655ad
add check for unallowed-roles on header elm
Zidious Oct 15, 2021
02d38a1
add role=contentinfo footer html elm
Zidious Oct 15, 2021
843ef70
changes requested
Zidious Oct 25, 2021
2b031c5
changes requested
Zidious Oct 25, 2021
ff147d6
changes requested
Zidious Oct 27, 2021
6eb5ef2
clean up dpub if-block
Zidious Oct 27, 2021
2caa508
add dpub role tests
Zidious Oct 27, 2021
6138c76
Merge branch 'develop' of https://github.com/dequelabs/axe-core into …
Zidious Nov 2, 2021
de1480d
changes requested
Zidious Nov 2, 2021
ed7d7a7
Merge branch 'develop' of https://github.com/dequelabs/axe-core into …
Zidious Nov 9, 2021
f3406bf
add tests for roles on axe.configure and add tr case
Zidious Nov 9, 2021
dd6e828
changes requested
Zidious Nov 10, 2021
b1ee30f
chore(getElementUnallowedRoles): Clean-up work
WilcoFiers Nov 10, 2021
e8fe28c
chore: minor tweak
WilcoFiers Nov 10, 2021
2c8134e
Merge branch 'develop' into headerrole3108
Zidious Nov 11, 2021
9988ebd
fix integration tests
Zidious Nov 11, 2021
2df5d81
Merge branch 'develop' into headerrole3108
Zidious Nov 13, 2021
525f701
chore(typo): Update lib/commons/aria/is-aria-role-allowed-on-element.js
WilcoFiers Nov 15, 2021
2bec405
fix merge conflicts
Zidious Nov 17, 2021
00dbbc1
fix merge conflicts
Zidious Nov 17, 2021
459eead
add TR test case back
Zidious Nov 18, 2021
0ea15d3
Merge branch 'develop' of https://github.com/dequelabs/axe-core into …
Zidious Nov 18, 2021
ec6047f
merge changes
Zidious Nov 18, 2021
9170f27
remove duplicate integration tests
Zidious Nov 18, 2021
d8f858b
Merge branch 'develop' of https://github.com/dequelabs/axe-core into …
Zidious Nov 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 26 additions & 47 deletions lib/commons/aria/get-element-unallowed-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ import isValidRole from './is-valid-role';
import getImplicitRole from './implicit-role';
import getRoleType from './get-role-type';
import isAriaRoleAllowedOnElement from './is-aria-role-allowed-on-element';
import {
tokenList,
isHtmlElement,
matchesSelector,
getNodeFromTree
} from '../../core/utils';
import { tokenList, isHtmlElement, getNodeFromTree } from '../../core/utils';
import AbstractVirtuaNode from '../../core/base/virtual-node/abstract-virtual-node';

// dpub roles which are subclassing roles that are implicit on some native
Expand All @@ -22,6 +17,11 @@ const dpubRoles = [
'doc-noteref'
];

const landmarkRoles = {
Zidious marked this conversation as resolved.
Show resolved Hide resolved
header: 'banner',
footer: 'contentinfo'
};

/**
* Returns all roles applicable to element in a list
*
Expand All @@ -33,7 +33,6 @@ const dpubRoles = [

function getRoleSegments(vNode) {
let roles = [];

if (!vNode) {
return roles;
}
Expand All @@ -44,9 +43,7 @@ function getRoleSegments(vNode) {
}

// filter invalid roles
roles = roles.filter(role => isValidRole(role));

return roles;
return roles.filter(role => isValidRole(role));
}

/**
Expand All @@ -59,50 +56,32 @@ function getRoleSegments(vNode) {
function getElementUnallowedRoles(node, allowImplicit = true) {
const vNode =
node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node);
const { nodeName } = vNode.props;

// by pass custom elements
if (!isHtmlElement(vNode)) {
return [];
}

// allow landmark roles to use their implicit role inside another landmark
// @see https://github.com/dequelabs/axe-core/pull/3142
const { nodeName } = vNode.props;
const implicitRole = getImplicitRole(vNode) || landmarkRoles[nodeName];
dylanb marked this conversation as resolved.
Show resolved Hide resolved

const roleSegments = getRoleSegments(vNode);
const implicitRole = getImplicitRole(vNode);

// 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 role and implicit role are same, when allowImplicit: true
// ignore as it is a redundant role
if (allowImplicit && role === implicitRole) {
return false;
}

// if role is a dpub role make sure it's used on an element with a valid
// implicit role fallback
if (allowImplicit && dpubRoles.includes(role)) {
const roleType = getRoleType(role);
if (implicitRole !== roleType) {
return true;
}
}

// Edge case:
// setting implicit role row on tr element is allowed when child of table[role='grid']
if (
!allowImplicit &&
!(
role === 'row' &&
nodeName === 'tr' &&
matchesSelector(vNode, 'table[role="grid"] > tr')
)
) {
return true;
}
// check if role is allowed on element
return !isAriaRoleAllowedOnElement(vNode, role);
return roleSegments.filter(role => {
return !roleIsAllowed(role, vNode, allowImplicit, implicitRole)
});
}

return unallowedRoles;
function roleIsAllowed(role, vNode, allowImplicit, implicitRole) {
if (allowImplicit && role === implicitRole) {
return true;
}
// if role is a dpub role make sure it's used on an element with a valid
// implicit role fallback
if (dpubRoles.includes(role) && getRoleType(role) !== implicitRole) {
return false;
}
// check if role is allowed on element
return isAriaRoleAllowedOnElement(vNode, role);
}

export default getElementUnallowedRoles;
10 changes: 5 additions & 5 deletions lib/commons/aria/is-aria-role-allowed-on-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ function isAriaRoleAllowedOnElement(node, role) {
node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node);
const implicitRole = getImplicitRole(vNode);

// always allow the explicit role to match the implicit role
if (role === implicitRole) {
return true;
}

const spec = getElementSpec(vNode);

if (Array.isArray(spec.allowedRoles)) {
return spec.allowedRoles.includes(role);
}

// By default, ARIA in HTLM does not allow implicit roles to be the same as explicit ones
WilcoFiers marked this conversation as resolved.
Show resolved Hide resolved
// aria-allowed-roles has an `allowedImplicit` option to bypass this.
if (role === implicitRole) {
return false;
}
return !!spec.allowedRoles;
}

Expand Down
6 changes: 3 additions & 3 deletions test/checks/aria/aria-allowed-role.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ describe('aria-allowed-role', function() {
var options = {
allowImplicit: false
};
var actual = axe.testUtils
var outcome = axe.testUtils
.getCheckEvaluate('aria-allowed-role')
.call(checkContext, null, options, vNode);
var expected = false;
assert.equal(actual, expected);

assert.isFalse(outcome);
assert.deepEqual(checkContext._data, ['row']);
});

Expand Down
131 changes: 116 additions & 15 deletions test/commons/aria/get-element-unallowed-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ describe('aria.getElementUnallowedRoles', function() {
var flatTreeSetup = axe.testUtils.flatTreeSetup;
var getElementUnallowedRoles = axe.commons.aria.getElementUnallowedRoles;

it('returns false for INPUT with role application', function() {
it('returns unallowed role=application when used on a input elm', function() {
var node = document.createElement('input');
var role = 'application';
node.setAttribute('type', '');
Expand All @@ -14,7 +14,7 @@ describe('aria.getElementUnallowedRoles', function() {
assert.include(actual, role);
});

it('returns true for INPUT type is checkbox and has aria-pressed attribute', function() {
it('returns empty on type=checkbox and aria-pressed attr on input elm', function() {
var node = document.createElement('input');
node.setAttribute('type', 'checkbox');
node.setAttribute('aria-pressed', '');
Expand All @@ -23,16 +23,16 @@ describe('aria.getElementUnallowedRoles', function() {
assert.isEmpty(actual);
});

it('returns false for LI with role menubar', function() {
it('returns unallowed role=menubar when used on a li elm', function() {
var node = document.createElement('li');
var role = 'menubar';
node.setAttribute('role', role);
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node);
assert.isNotEmpty(actual);
assert.isNotEmpty(actual, role);
});

it('returns true for INPUT with type button and role menuitemcheckbox', function() {
it('returns empty on role=menuitemcheckbox with type=button on input elm', function() {
var node = document.createElement('input');
var role = 'menuitemcheckbox';
node.setAttribute('role', role);
Expand All @@ -42,7 +42,7 @@ describe('aria.getElementUnallowedRoles', function() {
assert.isEmpty(actual);
});

it('returns false for SECTION with role option', function() {
it('returns unallowed role=option when used on section elm', function() {
var node = document.createElement('section');
var role = 'option';
node.setAttribute('role', role);
Expand All @@ -52,7 +52,7 @@ describe('aria.getElementUnallowedRoles', function() {
assert.include(actual, role);
});

it('returns true for INPUT type radio with role menuitemradio', function() {
it('returns empty on role=menuitemradio and type=radio on input elm', function() {
var node = document.createElement('input');
var role = 'menuitemradio';
node.setAttribute('role', role);
Expand All @@ -62,25 +62,126 @@ describe('aria.getElementUnallowedRoles', function() {
assert.isEmpty(actual);
});

it('returns false when role is implicit and allowImplicit is true (default)', function() {
it('returns unallowed role=textbox on a input elm and allowImplicit is true (default)', function() {
var node = document.createElement('input');
var role = 'textbox';
node.setAttribute('role', role);
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, true);
assert.isEmpty(actual, role);
});

it('returns empty on role=button on div elm when role is not implicit and allowImplicit: false', function() {
var node = document.createElement('div');
node.setAttribute('role', 'button');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, false);
assert.isEmpty(actual);
});

it('returns false with implicit role of row for TR when allowImplicit is set to false via options', function() {
Zidious marked this conversation as resolved.
Show resolved Hide resolved
var node = document.createElement('tr');
node.setAttribute('role', 'row');
it('returns unallowed role=contentinfo on footer elm and allowImplicit: false', function() {
var node = document.createElement('footer');
node.setAttribute('role', 'contentinfo');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, false);
assert.isNotEmpty(actual);
assert.include(actual, 'row');
assert.isNotEmpty(actual, 'contentinfo');
});

it('returns unallowed role=banner on header elm and allowImplicit:false', function() {
var node = document.createElement('header');
node.setAttribute('role', 'banner');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, false);
assert.isNotEmpty(actual, 'banner');
});

it('returns empty on role=contentinfo on footer elm when allowImplicit:true', function() {
var node = document.createElement('footer');
node.setAttribute('role', 'contentinfo');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node);
assert.isEmpty(actual);
});

it('returns empty on role=banner on header elm when allowImplicit:true', function() {
var node = document.createElement('header');
node.setAttribute('role', 'banner');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node);
assert.isEmpty(actual);
});

it('returns empty role=doc-backlink on anchor elm and allowImplicit:false', function() {
var node = document.createElement('a');
node.setAttribute('href', '#');
node.setAttribute('role', 'doc-backlink');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, false);
assert.isEmpty(actual);
});

it('returns empty on role=doc-backlink on anchor elm when allowImplicit:true', function() {
var node = document.createElement('a');
node.setAttribute('href', '#');
node.setAttribute('role', 'doc-backlink');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node);
assert.isEmpty(actual);
});

it('returns unallowed role=doc-backlink on anchor elm without href attr and allowImplicit:false', function() {
var node = document.createElement('a');
node.setAttribute('role', 'doc-backlink');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, false);
assert.isNotEmpty(actual, 'doc-backlink');
});

it('returns unallowed role=doc-backlink on anchor elm without href attr and allowImplicit:true', function() {
var node = document.createElement('a');
node.setAttribute('role', 'doc-backlink');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node);
assert.isNotEmpty(actual, 'doc-backlink');
});

it('returns empty role=banner on header elm when using axe.configure and allowImplicit:false', function() {
axe.configure({
standards: {
htmlElms: {
header: {
contentTypes: 'flow',
allowedRoles: ['banner']
}
}
}
});
var node = document.createElement('header');
node.setAttribute('role', 'banner');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, false);
assert.isEmpty(actual);
});

it('returns empty role=contentinfo on footer elm when using axe.configure and allowImplicit:false', function() {
axe.configure({
standards: {
htmlElms: {
footer: {
contentTypes: 'flow',
allowedRoles: ['contentinfo']
}
}
}
});
var node = document.createElement('footer');
node.setAttribute('role', 'contentinfo');
flatTreeSetup(node);
var actual = getElementUnallowedRoles(node, false);
assert.isEmpty(actual);
});

it('returns true for a SerialVirtualNode of INPUT with type checkbox and aria-pressed attribute', function() {
it('returns empty on type=checkbox and aria-pressed attr on SerialVirtualNode with a input elm', function() {
var vNode = new axe.SerialVirtualNode({
nodeName: 'input',
attributes: {
Expand All @@ -92,7 +193,7 @@ describe('aria.getElementUnallowedRoles', function() {
assert.isEmpty(actual);
});

it('returns false for a SerialVirtualNode of INPUT with role application', function() {
it('returns unallowed role=application for a SerialVirtualNode with a input elm', function() {
var vNode = new axe.SerialVirtualNode({
nodeName: 'input',
attributes: {
Expand Down
4 changes: 2 additions & 2 deletions test/commons/aria/is-aria-role-allowed-on-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ describe('aria.isAriaRoleAllowedOnElement', function() {
assert.isFalse(actual);
});

it('returns true if elements implicit role matches the role', function() {
it('returns false if elements implicit role matches the role', function() {
var node = document.createElement('area');
node.setAttribute('href', '#yay');
node.setAttribute('role', 'link');
flatTreeSetup(node);
var actual = axe.commons.aria.isAriaRoleAllowedOnElement(node, 'link');
assert.isTrue(actual);
assert.isFalse(actual);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ <h1 role="text" id="fail-text-1">ok</h1>
<button role="text" id="fail-text-2">ok</button>
<a href="#" role="text" id="fail-text-3">ok</a>
<main role="text" id="fail-text-4">ok</main>
<main><header role="banner" id="pass-header-banner">ok</header></main>
<main><footer role="contentinfo" id="pass-footer-contentinfo">ok</footer></main>

<img usemap="#my-map">
<map name="my-map">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
["#pass-graphics-document"],
["#pass-graphics-object"],
["#pass-graphics-symbol"],
["#pass-header-banner"],
["#pass-footer-contentinfo"],
["#pass-img-valid-role-aria-label"],
["#pass-img-valid-role-title"],
["#pass-img-valid-role-aria-labelledby"],
Expand Down