Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7da6bec
Phase 1: Adding New Rules
gaganmBrowserStack Dec 8, 2025
bc346a3
Initial merge
gaganmBrowserStack Dec 9, 2025
e78afa3
Phase1.25
gaganmBrowserStack Dec 15, 2025
4480a9a
minor changes
gaganmBrowserStack Dec 16, 2025
dbb5108
Minor changes
gaganmBrowserStack Dec 17, 2025
86f13b9
Minor Changes
gaganmBrowserStack Dec 17, 2025
4d88749
Should change reverted
gaganmBrowserStack Dec 17, 2025
66b6d94
Reverting old changes
gaganmBrowserStack Dec 17, 2025
be6d6ca
Making heading-order & heading-order-bp stable
Aaryan430 Dec 18, 2025
1f75ff3
Added accuracy bucket
Aaryan430 Dec 18, 2025
edd8944
Merge pull request #173 from browserstack/AXE-2606-heading-order-stable
Aaryan430 Dec 18, 2025
2a7bdc8
"Ensure" compliance with changes
gaganmBrowserStack Dec 18, 2025
5eebebb
reverting labelContentNameMismatchEvaluate changes it should be in Ph…
gaganmBrowserStack Dec 19, 2025
8ca765f
Resolving PR review comments
gaganmBrowserStack Dec 22, 2025
271dceb
reverting minor change
gaganmBrowserStack Dec 22, 2025
a1d4fb9
validate.js
Aaryan430 Dec 23, 2025
3e4d665
json update
Aaryan430 Dec 23, 2025
b3f6774
json update
Aaryan430 Dec 23, 2025
1cca3ae
updating _template.json
gaganmBrowserStack Dec 23, 2025
8ce63e3
minor fix for eslint issue
gaganmBrowserStack Dec 23, 2025
d14e9de
Merge branch 'Upgrade-to-4.11' of https://github.com/browserstack/a11…
gaganmBrowserStack Dec 23, 2025
b45d65a
:robot: Automated formatting fixes
gaganmBrowserStack Dec 23, 2025
8bcbcfb
Merge branch 'release-5.14.0' of https://github.com/browserstack/a11y…
gaganmBrowserStack Dec 23, 2025
ab3113e
Merge pull request #172 from browserstack/Upgrade-to-4.11
gaganmBrowserStack Dec 23, 2025
166f994
5.14.0 pull
Aaryan430 Dec 26, 2025
9a9847a
Merge pull request #176 from browserstack/updating-accuracy-taggings
Aaryan430 Dec 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
108 changes: 108 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion build/cherry-pick.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env node

const { execSync } = require('child_process');
const conventionalCommitsParser = require('conventional-commits-parser');
const chalk = require('chalk');
Expand Down Expand Up @@ -129,7 +131,7 @@ commitsToCherryPick.forEach(({ hash, type, scope, subject }) => {

try {
execSync(`git cherry-pick ${hash} -X theirs`);
} catch (e) {
} catch {
console.error(
chalk.red.bold('\nAborting cherry-pick and reseting to master')
);
Expand Down
2 changes: 1 addition & 1 deletion build/tasks/metadata-function-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module.exports = function (grunt) {
'// This file is automatically generated using build/tasks/metadata-function-map.js\n';

src.forEach(globPath => {
glob.sync(globPath).forEach(filePath => {
glob.sync(globPath, { posix: true }).forEach(filePath => {
const relativePath = path.relative(
path.dirname(file.dest),
filePath
Expand Down
16 changes: 12 additions & 4 deletions build/tasks/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function fileExists(v, o) {
var exists;
try {
exists = fs.existsSync(file);
} catch (e) {
} catch {
return false;
}
return exists;
Expand Down Expand Up @@ -316,7 +316,9 @@ const miscTags = [
'a11y-engine-experimental',
'advanced',
'ai',
'new'
'new',
'medium-accuracy',
'high-accuracy'
];

const categories = [
Expand Down Expand Up @@ -361,6 +363,12 @@ const standardsTags = [
standardRegex: /^EN-301-549$/,
criterionRegex: /^EN-9\.[1-4]\.[1-9]\.\d{1,2}$/,
wcagLevelRegex: /^wcag21?aa?$/
},
{
name: 'RGAA',
standardRegex: /^RGAAv4$/,
criterionRegex: /^RGAA-\d{1,2}\.\d{1,2}\.\d{1,2}$/,
wcagLevelRegex: /^wcag21?aa?$/
}
];

Expand Down Expand Up @@ -411,7 +419,7 @@ function findTagIssues(tags) {
standardTag: standardTags[0] ?? null,
criterionTags
};
if (bestPracticeTags.length !== 0) {
if (name !== 'RGAA' && bestPracticeTags.length !== 0) {
issues.push(`${name} tags cannot be used along side best-practice tag`);
}
if (standardTags.length === 0) {
Expand All @@ -423,7 +431,7 @@ function findTagIssues(tags) {
issues.push(`Expected at least one ${name} criterion tag, got 0`);
}

if (wcagLevelRegex) {
if (wcagLevelRegex && standards.WCAG) {
const wcagLevel = standards.WCAG.standardTag;
if (!wcagLevel.match(wcagLevelRegex)) {
issues.push(`${name} rules not allowed on ${wcagLevel}`);
Expand Down
4 changes: 2 additions & 2 deletions doc/examples/qunit/test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
<!-- Load local QUnit. -->
<link
rel="stylesheet"
href="../node_modules/qunitjs/qunit/qunit.css"
href="../node_modules/qunit/qunit/qunit.css"
media="screen"
/>
<script src="../node_modules/qunitjs/qunit/qunit.js"></script>
<script src="../node_modules/qunit/qunit/qunit.js"></script>
<!-- Load local lib and tests. -->
<script src="../node_modules/axe-core/axe.min.js"></script>
<script src="a11y.js"></script>
Expand Down
230 changes: 116 additions & 114 deletions doc/rule-descriptions.md

Large diffs are not rendered by default.

26 changes: 25 additions & 1 deletion lib/checks/aria/aria-allowed-attr-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export default function ariaAllowedAttrEvaluate(node, options, virtualNode) {

// Unknown ARIA attributes are tested in aria-valid-attr
for (const attrName of virtualNode.attrNames) {
if (validateAttr(attrName) && !allowed.includes(attrName)) {
if (
validateAttr(attrName) &&
!allowed.includes(attrName) &&
!ignoredAttrs(attrName, virtualNode.attr(attrName), virtualNode)
) {
invalid.push(attrName);
}
}
Expand All @@ -57,3 +61,23 @@ export default function ariaAllowedAttrEvaluate(node, options, virtualNode) {
}
return false;
}

function ignoredAttrs(attrName, attrValue, vNode) {
// allow aria-required=false as screen readers consistently ignore it
// @see https://github.com/dequelabs/axe-core/issues/3756
if (attrName === 'aria-required' && attrValue === 'false') {
return true;
}

// allow aria-multiline=false when contenteditable is set
// @see https://github.com/dequelabs/axe-core/issues/4463
if (
attrName === 'aria-multiline' &&
attrValue === 'false' &&
vNode.hasAttr('contenteditable')
) {
return true;
}

return false;
}
8 changes: 4 additions & 4 deletions lib/checks/aria/aria-errormessage-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import standards from '../../standards';
import { idrefs } from '../../commons/dom';
import { idrefs, isVisibleToScreenReaders } from '../../commons/dom';
import { tokenList } from '../../core/utils';
import { isVisibleToScreenReaders } from '../../commons/dom';
import { getExplicitRole } from '../../commons/aria';
/**
* Check if `aria-errormessage` references an element that also uses a technique to announce the message (aria-live, aria-describedby, etc.).
*
Expand Down Expand Up @@ -46,7 +46,7 @@ export default function ariaErrormessageEvaluate(node, options, virtualNode) {

try {
idref = attr && idrefs(virtualNode, 'aria-errormessage')[0];
} catch (e) {
} catch {
this.data({
messageKey: 'idrefs',
values: tokenList(attr)
Expand All @@ -63,7 +63,7 @@ export default function ariaErrormessageEvaluate(node, options, virtualNode) {
return false;
}
return (
idref.getAttribute('role') === 'alert' ||
getExplicitRole(idref) === 'alert' ||
idref.getAttribute('aria-live') === 'assertive' ||
idref.getAttribute('aria-live') === 'polite' ||
tokenList(virtualNode.attr('aria-describedby')).indexOf(attr) > -1
Expand Down
6 changes: 3 additions & 3 deletions lib/checks/aria/aria-errormessage.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
"hidden": "aria-errormessage value `${data.values}` cannot reference a hidden element"
},
"incomplete": {
"singular": "ensure aria-errormessage value `${data.values}` references an existing element",
"plural": "ensure aria-errormessage values `${data.values}` reference existing elements",
"idrefs": "unable to determine if aria-errormessage element exists on the page: ${data.values}"
"singular": "Ensure aria-errormessage value `${data.values}` references an existing element",
"plural": "Ensure aria-errormessage values `${data.values}` reference existing elements",
"idrefs": "Unable to determine if aria-errormessage element exists on the page: ${data.values}"
}
}
}
Expand Down
35 changes: 30 additions & 5 deletions lib/checks/aria/aria-prohibited-attr-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getRole } from '../../commons/aria';
import { getRole, getRoleType } from '../../commons/aria';
import { sanitize, subtreeText } from '../../commons/text';
import standards from '../../standards';
import memoize from '../../core/utils/memoize';

/**
* Check that an element does not use any prohibited ARIA attributes.
Expand Down Expand Up @@ -33,9 +34,14 @@ export default function ariaProhibitedAttrEvaluate(
) {
const elementsAllowedAriaLabel = options?.elementsAllowedAriaLabel || [];
const { nodeName } = virtualNode.props;
const role = getRole(virtualNode, { chromium: true });
const role = getRole(virtualNode, {
chromium: true,
// this check allows fallback roles. For example, `<div role="foo img" aria-label="...">` is legal.
fallback: true
});

const prohibitedList = listProhibitedAttrs(
virtualNode,
role,
nodeName,
elementsAllowedAriaLabel
Expand All @@ -51,7 +57,7 @@ export default function ariaProhibitedAttrEvaluate(
return false;
}

let messageKey = virtualNode.hasAttr('role') ? 'hasRole' : 'noRole';
let messageKey = role !== null ? 'hasRole' : 'noRole';
messageKey += prohibited.length > 1 ? 'Plural' : 'Singular';
this.data({ role, nodeName, messageKey, prohibited });

Expand All @@ -64,13 +70,32 @@ export default function ariaProhibitedAttrEvaluate(
return true;
}

function listProhibitedAttrs(role, nodeName, elementsAllowedAriaLabel) {
function listProhibitedAttrs(vNode, role, nodeName, elementsAllowedAriaLabel) {
const roleSpec = standards.ariaRoles[role];
if (roleSpec) {
return roleSpec.prohibitedAttrs || [];
}
if (!!role || elementsAllowedAriaLabel.includes(nodeName)) {
if (
!!role ||
elementsAllowedAriaLabel.includes(nodeName) ||
getClosestAncestorRoleType(vNode) === 'widget'
) {
return [];
}
return ['aria-label', 'aria-labelledby'];
}

const getClosestAncestorRoleType = memoize(
function getClosestAncestorRoleTypeMemoized(vNode) {
if (!vNode) {
return;
}

const role = getRole(vNode, { noPresentational: true, chromium: true });
if (role) {
return getRoleType(role);
}

return getClosestAncestorRoleType(vNode.parent);
}
);
5 changes: 5 additions & 0 deletions lib/checks/aria/aria-required-attr-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export default function ariaRequiredAttrEvaluate(
) {
return true;
}
// Non-normative exception for things like media player seek slider.
// Tested to work in various screen readers.
if (role === 'slider' && virtualNode.attr('aria-valuetext')?.trim()) {
return true;
}

const elmSpec = getElementSpec(virtualNode);
const missingAttrs = requiredAttrs.filter(
Expand Down
4 changes: 2 additions & 2 deletions lib/checks/aria/aria-required-parent-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ function getMissingContext(
}

function getAriaOwners(element) {
var owners = [],
o = null;
const owners = [];
let o = null;

while (element) {
if (element.getAttribute('id')) {
Expand Down
17 changes: 14 additions & 3 deletions lib/checks/aria/aria-valid-attr-value-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,23 @@ export default function ariaValidAttrValueEvaluate(node, options, virtualNode) {

const preChecks = {
// aria-controls should only check if element exists if the element
// doesn't have aria-expanded=false or aria-selected=false (tabs)
// doesn't have aria-expanded=false, aria-selected=false (tabs),
// or aria-haspopup (may load later)
// @see https://github.com/dequelabs/axe-core/issues/1463
// @see https://github.com/dequelabs/axe-core/issues/4363
'aria-controls': () => {
const hasPopup =
['false', null].includes(virtualNode.attr('aria-haspopup')) === false;

if (hasPopup) {
needsReview = `aria-controls="${virtualNode.attr('aria-controls')}"`;
messageKey = 'controlsWithinPopup';
}

return (
virtualNode.attr('aria-expanded') !== 'false' &&
virtualNode.attr('aria-selected') !== 'false'
virtualNode.attr('aria-selected') !== 'false' &&
hasPopup === false
);
},
// aria-current should mark as needs review if any value is used that is
Expand Down Expand Up @@ -104,7 +115,7 @@ export default function ariaValidAttrValueEvaluate(node, options, virtualNode) {

try {
validValue = validateAttrValue(virtualNode, attrName);
} catch (e) {
} catch {
needsReview = `${attrName}="${attrValue}"`;
messageKey = 'idrefs';
return;
Expand Down
3 changes: 2 additions & 1 deletion lib/checks/aria/aria-valid-attr-value.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"noIdShadow": "ARIA attribute element ID does not exist on the page or is a descendant of a different shadow DOM tree: ${data.needsReview}",
"ariaCurrent": "ARIA attribute value is invalid and will be treated as \"aria-current=true\": ${data.needsReview}",
"idrefs": "Unable to determine if ARIA attribute element ID exists on the page: ${data.needsReview}",
"empty": "ARIA attribute value is ignored while empty: ${data.needsReview}"
"empty": "ARIA attribute value is ignored while empty: ${data.needsReview}",
"controlsWithinPopup": "Unable to determine if aria-controls referenced ID exists on the page while using aria-haspopup: ${data.needsReview}"
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions lib/checks/aria/has-widget-role-evaluate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getRoleType } from '../../commons/aria';
import { getRoleType, getExplicitRole } from '../../commons/aria';

/**
* Check if an elements `role` attribute uses any widget or composite role values.
Expand All @@ -8,8 +8,8 @@ import { getRoleType } from '../../commons/aria';
* @memberof checks
* @return {Boolean} True if the element uses a `widget` or `composite` role. False otherwise.
*/
function hasWidgetRoleEvaluate(node) {
const role = node.getAttribute('role');
function hasWidgetRoleEvaluate(node, options, virtualNode) {
const role = getExplicitRole(virtualNode);
if (role === null) {
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/aria/invalidrole-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { tokenList } from '../../core/utils';
function invalidroleEvaluate(node, options, virtualNode) {
const allRoles = tokenList(virtualNode.attr('role'));
const allInvalid = allRoles.every(
role => !isValidRole(role, { allowAbstract: true })
role => !isValidRole(role.toLowerCase(), { allowAbstract: true })
);

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/checks/aria/no-implicit-explicit-label-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function noImplicitExplicitLabelEvaluate(node, options, virtualNode) {
try {
label = sanitize(labelText(virtualNode)).toLowerCase();
accText = sanitize(accessibleTextVirtual(virtualNode)).toLowerCase();
} catch (e) {
} catch {
return undefined;
}

Expand Down
10 changes: 8 additions & 2 deletions lib/checks/aria/valid-scrollable-semantics-evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@ const VALID_TAG_NAMES_FOR_SCROLLABLE_REGIONS = {
* appropriate for scrollable elements found in the focus order.
*/
const VALID_ROLES_FOR_SCROLLABLE_REGIONS = {
alert: true,
alertdialog: true,
application: true,
article: true,
banner: false,
complementary: true,
contentinfo: true,
dialog: true,
form: true,
log: true,
main: true,
navigation: true,
region: true,
search: false
search: false,
status: true,
tabpanel: true
};

/**
Expand All @@ -46,7 +52,7 @@ function validScrollableTagName(node) {
* region.
*/
function validScrollableRole(node, options) {
var role = getExplicitRole(node);
const role = getExplicitRole(node);
if (!role) {
return false;
}
Expand Down
Loading
Loading