Skip to content

Commit

Permalink
feat(aria-required-children): fail for children which are not listed …
Browse files Browse the repository at this point in the history
…as required (#3597)

* fix(aria-required-children): fail for child elements which are not listed as required

* Update lib/checks/aria/aria-required-children-evaluate.js

* Update lib/checks/aria/aria-required-children-evaluate.js

* Update lib/checks/aria/aria-required-children-evaluate.js

* fix apg

* update tests

* test

* Update lib/checks/aria/aria-required-children.json

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>

Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
  • Loading branch information
straker and WilcoFiers committed Aug 18, 2022
1 parent b4162ed commit b5ceabc
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 55 deletions.
31 changes: 24 additions & 7 deletions lib/checks/aria/aria-required-children-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
getExplicitRole,
getOwnedVirtual
} from '../../commons/aria';
import { hasContentVirtual, idrefs } from '../../commons/dom';
import { getGlobalAriaAttrs } from '../../commons/standards';
import { hasContentVirtual, idrefs, isFocusable } from '../../commons/dom';

/**
* Get all owned roles of an element
Expand All @@ -16,18 +17,24 @@ function getOwnedRoles(virtualNode, required) {
const ownedElement = ownedElements[i];
const role = getRole(ownedElement, { noPresentational: true });

const hasGlobalAria = getGlobalAriaAttrs().some(attr =>
ownedElement.hasAttr(attr)
);
const hasGlobalAriaOrFocusable =
hasGlobalAria || isFocusable(ownedElement);

// if owned node has no role or is presentational, or if role
// allows group or rowgroup, we keep parsing the descendant tree.
// this means intermediate roles between a required parent and
// child will fail the check
if (
!role ||
(!role && ! hasGlobalAriaOrFocusable) ||
(['group', 'rowgroup'].includes(role) &&
required.some(requiredRole => requiredRole === role))
) {
ownedElements.push(...ownedElement.children);
} else if (role) {
ownedRoles.push(role);
} else if (role || hasGlobalAriaOrFocusable) {
ownedRoles.push({ role, ownedElement });
}
}

Expand All @@ -39,10 +46,10 @@ function getOwnedRoles(virtualNode, required) {
*/
function missingRequiredChildren(virtualNode, role, required, ownedRoles) {
for (let i = 0; i < ownedRoles.length; i++) {
var ownedRole = ownedRoles[i];
const { role } = ownedRoles[i];

if (required.includes(ownedRole)) {
required = required.filter(requiredRole => requiredRole !== ownedRole);
if (required.includes(role)) {
required = required.filter(requiredRole => requiredRole !== role);
return null;
}
}
Expand Down Expand Up @@ -74,6 +81,16 @@ function ariaRequiredChildrenEvaluate(node, options, virtualNode) {
}

const ownedRoles = getOwnedRoles(virtualNode, required);
const unallowed = ownedRoles.filter(({ role }) => !required.includes(role));

if (unallowed.length) {
this.relatedNodes(unallowed.map(({ ownedElement }) => ownedElement));
this.data({
messageKey: 'unallowed'
});
return false;
}

const missing = missingRequiredChildren(
virtualNode,
role,
Expand Down
3 changes: 2 additions & 1 deletion lib/checks/aria/aria-required-children.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"pass": "Required ARIA children are present",
"fail": {
"singular": "Required ARIA child role not present: ${data.values}",
"plural": "Required ARIA children role not present: ${data.values}"
"plural": "Required ARIA children role not present: ${data.values}",
"unallowed": "Element has children which are not allowed (see related nodes)"
},
"incomplete": {
"singular": "Expecting ARIA child role to be added: ${data.values}",
Expand Down
11 changes: 9 additions & 2 deletions lib/standards/aria-roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,14 @@ const ariaRoles = {
},
menubar: {
type: 'composite',
requiredOwned: ['group', 'menuitemradio', 'menuitem', 'menuitemcheckbox'],
// Note: spec difference (menu as required owned)
requiredOwned: [
'group',
'menuitemradio',
'menuitem',
'menuitemcheckbox',
'menu'
],
allowedAttrs: [
'aria-activedescendant',
'aria-expanded',
Expand Down Expand Up @@ -477,7 +484,7 @@ const ariaRoles = {
},
radiogroup: {
type: 'composite',
requiredOwned: ['radio'],
// Note: spec difference (no required owned)
allowedAttrs: [
'aria-readonly',
'aria-required',
Expand Down
Loading

0 comments on commit b5ceabc

Please sign in to comment.