From 53fb2818730b4435fb3aa5c8d83baafe3d23fc4f Mon Sep 17 00:00:00 2001 From: Dan Tripp Date: Sat, 26 Feb 2022 18:38:48 -0500 Subject: [PATCH] fix(rules): handle roles case-insensitively Closes part of #2695 --- lib/checks/aria/aria-errormessage-evaluate.js | 2 +- lib/checks/aria/fallbackrole-evaluate.js | 2 +- lib/checks/aria/invalidrole-evaluate.js | 2 +- lib/checks/keyboard/landmark-is-top-level-evaluate.js | 2 +- lib/checks/tables/th-has-data-cells-evaluate.js | 2 +- lib/commons/aria/get-role-type.js | 2 +- lib/commons/aria/is-unsupported-role.js | 2 +- lib/commons/aria/is-valid-role.js | 2 +- lib/commons/dom/is-visual-content.js | 2 +- lib/commons/table/get-scope.js | 2 +- lib/commons/table/is-data-cell.js | 2 +- lib/rules/autocomplete-matches.js | 2 +- lib/rules/heading-matches.js | 1 + lib/rules/link-in-text-block-matches.js | 2 +- lib/rules/scrollable-region-focusable-matches.js | 2 +- 15 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/checks/aria/aria-errormessage-evaluate.js b/lib/checks/aria/aria-errormessage-evaluate.js index bacb1c9c7c..205e18350a 100644 --- a/lib/checks/aria/aria-errormessage-evaluate.js +++ b/lib/checks/aria/aria-errormessage-evaluate.js @@ -63,7 +63,7 @@ function ariaErrormessageEvaluate(node, options, virtualNode) { return false; } return ( - idref.getAttribute('role') === 'alert' || + idref.getAttribute('role')?.toLowerCase() === 'alert' || idref.getAttribute('aria-live') === 'assertive' || idref.getAttribute('aria-live') === 'polite' || tokenList(virtualNode.attr('aria-describedby')).indexOf(attr) > -1 diff --git a/lib/checks/aria/fallbackrole-evaluate.js b/lib/checks/aria/fallbackrole-evaluate.js index 1aec63d78c..0b167d0908 100644 --- a/lib/checks/aria/fallbackrole-evaluate.js +++ b/lib/checks/aria/fallbackrole-evaluate.js @@ -23,7 +23,7 @@ function nonePresentationOnElementWithNoImplicitRole( * @return {Boolean} True if the element uses more than one explicit role. False otherwise. */ function fallbackroleEvaluate(node, options, virtualNode) { - const explicitRoles = tokenList(virtualNode.attr('role')); + const explicitRoles = tokenList(virtualNode.attr('role')?.toLowerCase()); if (explicitRoles.length <= 1) { return false; } diff --git a/lib/checks/aria/invalidrole-evaluate.js b/lib/checks/aria/invalidrole-evaluate.js index 9c8f26ed61..8e0cc4f91a 100644 --- a/lib/checks/aria/invalidrole-evaluate.js +++ b/lib/checks/aria/invalidrole-evaluate.js @@ -26,7 +26,7 @@ import { tokenList } from '../../core/utils'; * @return {Boolean} True if the element uses an invalid role. False otherwise. */ function invalidroleEvaluate(node, options, virtualNode) { - const allRoles = tokenList(virtualNode.attr('role')); + const allRoles = tokenList(virtualNode.attr('role')?.toLowerCase()); const allInvalid = allRoles.every( role => !isValidRole(role, { allowAbstract: true }) ); diff --git a/lib/checks/keyboard/landmark-is-top-level-evaluate.js b/lib/checks/keyboard/landmark-is-top-level-evaluate.js index 15ba47200d..d526a0d099 100644 --- a/lib/checks/keyboard/landmark-is-top-level-evaluate.js +++ b/lib/checks/keyboard/landmark-is-top-level-evaluate.js @@ -10,7 +10,7 @@ function landmarkIsTopLevelEvaluate(node) { this.data({ role: nodeRole }); while (parent) { - var role = parent.getAttribute('role'); + var role = parent.getAttribute('role')?.toLowerCase(); if (!role && parent.nodeName.toUpperCase() !== 'FORM') { role = implicitRole(parent); } diff --git a/lib/checks/tables/th-has-data-cells-evaluate.js b/lib/checks/tables/th-has-data-cells-evaluate.js index 1c83ef03ed..1aadf15721 100644 --- a/lib/checks/tables/th-has-data-cells-evaluate.js +++ b/lib/checks/tables/th-has-data-cells-evaluate.js @@ -26,7 +26,7 @@ function thHasDataCellsEvaluate(node) { } return ( cell.nodeName.toUpperCase() === 'TH' || - ['rowheader', 'columnheader'].indexOf(cell.getAttribute('role')) !== -1 + ['rowheader', 'columnheader'].indexOf(cell.getAttribute('role')?.toLowerCase()) !== -1 ); }); diff --git a/lib/commons/aria/get-role-type.js b/lib/commons/aria/get-role-type.js index 84ca396ca3..3fed606edd 100644 --- a/lib/commons/aria/get-role-type.js +++ b/lib/commons/aria/get-role-type.js @@ -9,7 +9,7 @@ import standards from '../../standards'; * @return {Mixed} String if a matching role and its type are found, otherwise `null` */ function getRoleType(role) { - const roleDef = standards.ariaRoles[role]; + const roleDef = standards.ariaRoles[role?.toLowerCase()]; if (!roleDef) { return null; diff --git a/lib/commons/aria/is-unsupported-role.js b/lib/commons/aria/is-unsupported-role.js index eebc7b71dd..2b7bf9396b 100644 --- a/lib/commons/aria/is-unsupported-role.js +++ b/lib/commons/aria/is-unsupported-role.js @@ -9,7 +9,7 @@ import standards from '../../standards'; * @return {Boolean} */ function isUnsupportedRole(role) { - const roleDefinition = standards.ariaRoles[role]; + const roleDefinition = standards.ariaRoles[role?.toLowerCase()]; return roleDefinition ? !!roleDefinition.unsupported : false; } diff --git a/lib/commons/aria/is-valid-role.js b/lib/commons/aria/is-valid-role.js index 72a57eab51..8a6e48fff4 100644 --- a/lib/commons/aria/is-valid-role.js +++ b/lib/commons/aria/is-valid-role.js @@ -11,7 +11,7 @@ import isUnsupportedRole from './is-unsupported-role'; * @return {Boolean} */ function isValidRole(role, { allowAbstract, flagUnsupported = false } = {}) { - const roleDefinition = standards.ariaRoles[role]; + const roleDefinition = standards.ariaRoles[role?.toLowerCase()]; const isRoleUnsupported = isUnsupportedRole(role); if (!roleDefinition || (flagUnsupported && isRoleUnsupported)) { return false; diff --git a/lib/commons/dom/is-visual-content.js b/lib/commons/dom/is-visual-content.js index ef8b7c0aa8..a0efe53777 100644 --- a/lib/commons/dom/is-visual-content.js +++ b/lib/commons/dom/is-visual-content.js @@ -18,7 +18,7 @@ const visualRoles = [ */ function isVisualContent(element) { /*eslint indent: 0*/ - const role = element.getAttribute('role'); + const role = element.getAttribute('role')?.toLowerCase(); if (role) { return visualRoles.indexOf(role) !== -1; } diff --git a/lib/commons/table/get-scope.js b/lib/commons/table/get-scope.js index cef6cd8049..eb36f27b6e 100644 --- a/lib/commons/table/get-scope.js +++ b/lib/commons/table/get-scope.js @@ -12,7 +12,7 @@ import findUp from '../dom/find-up'; */ function getScope(cell) { var scope = cell.getAttribute('scope'); - var role = cell.getAttribute('role'); + var role = cell.getAttribute('role')?.toLowerCase(); if ( cell instanceof window.Element === false || diff --git a/lib/commons/table/is-data-cell.js b/lib/commons/table/is-data-cell.js index 8e1401db1d..691c7db38d 100644 --- a/lib/commons/table/is-data-cell.js +++ b/lib/commons/table/is-data-cell.js @@ -13,7 +13,7 @@ function isDataCell(cell) { if (!cell.children.length && !cell.textContent.trim()) { return false; } - const role = cell.getAttribute('role'); + const role = cell.getAttribute('role')?.toLowerCase(); if (isValidRole(role)) { return ['cell', 'gridcell'].includes(role); } else { diff --git a/lib/rules/autocomplete-matches.js b/lib/rules/autocomplete-matches.js index 88cb1454ac..7eaf605bb7 100644 --- a/lib/rules/autocomplete-matches.js +++ b/lib/rules/autocomplete-matches.js @@ -33,7 +33,7 @@ function autocompleteMatches(node, virtualNode) { // The element has `tabindex="-1"` and has a [[semantic role]] that is // not a [widget](https://www.w3.org/TR/wai-aria-1.1/#widget_roles) - const role = virtualNode.attr('role'); + const role = virtualNode.attr('role')?.toLowerCase(); const tabIndex = virtualNode.attr('tabindex'); if (tabIndex === '-1' && role) { const roleDef = standards.ariaRoles[role]; diff --git a/lib/rules/heading-matches.js b/lib/rules/heading-matches.js index fcfa3f670c..6f159bfb2d 100644 --- a/lib/rules/heading-matches.js +++ b/lib/rules/heading-matches.js @@ -4,6 +4,7 @@ function headingMatches(node) { if (node.hasAttribute('role')) { explicitRoles = node .getAttribute('role') + .toLowerCase() .split(/\s+/i) .filter(axe.commons.aria.isValidRole); } diff --git a/lib/rules/link-in-text-block-matches.js b/lib/rules/link-in-text-block-matches.js index 3ef3a50519..f71f1951f7 100644 --- a/lib/rules/link-in-text-block-matches.js +++ b/lib/rules/link-in-text-block-matches.js @@ -3,7 +3,7 @@ import { isVisible, isInTextBlock } from '../commons/dom'; function linkInTextBlockMatches(node) { var text = sanitize(node.textContent); - var role = node.getAttribute('role'); + var role = node.getAttribute('role')?.toLowerCase(); if (role && role !== 'link') { return false; diff --git a/lib/rules/scrollable-region-focusable-matches.js b/lib/rules/scrollable-region-focusable-matches.js index 71acf09d24..94f3769dca 100644 --- a/lib/rules/scrollable-region-focusable-matches.js +++ b/lib/rules/scrollable-region-focusable-matches.js @@ -44,7 +44,7 @@ function scrollableRegionFocusableMatches(node, virtualNode) { doc.querySelectorAll(`[aria-owns~="${id}"], [aria-controls~="${id}"]`) ); const comboboxOwned = owned.some(el => { - const roles = tokenList(el.getAttribute('role')); + const roles = tokenList(el.getAttribute('role')?.toLowerCase()); return roles.includes('combobox'); });