diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 570db45783..92893389c4 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -16,6 +16,7 @@ | :------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [area-alt](https://dequeuniversity.com/rules/axe/4.7/area-alt?application=RuleDescription) | Ensures <area> elements of image maps have alternate text | Critical | cat.text-alternatives, wcag2a, wcag244, wcag412, section508, section508.22.a, TTv5, TT6.a, EN-301-549, EN-9.2.4.4, EN-9.4.1.2, ACT | failure, needs review | [c487ae](https://act-rules.github.io/rules/c487ae) | | [aria-allowed-attr](https://dequeuniversity.com/rules/axe/4.7/aria-allowed-attr?application=RuleDescription) | Ensures an element's role supports its ARIA attributes | Critical | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | [5c01ea](https://act-rules.github.io/rules/5c01ea) | +| [aria-braille-equivalent](https://dequeuniversity.com/rules/axe/4.7/aria-braille-equivalent?application=RuleDescription) | Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure, needs review | | | [aria-command-name](https://dequeuniversity.com/rules/axe/4.7/aria-command-name?application=RuleDescription) | Ensures every ARIA button, link and menuitem has an accessible name | Serious | cat.aria, wcag2a, wcag412, TTv5, TT6.a, EN-301-549, EN-9.4.1.2, ACT | failure, needs review | [97a4e1](https://act-rules.github.io/rules/97a4e1) | | [aria-conditional-attr](https://dequeuniversity.com/rules/axe/4.7/aria-conditional-attr?application=RuleDescription) | Ensures ARIA attributes are used as described in the specification of the element's role | Serious | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [5c01ea](https://act-rules.github.io/rules/5c01ea) | | [aria-deprecated-role](https://dequeuniversity.com/rules/axe/4.7/aria-deprecated-role?application=RuleDescription) | Ensures elements do not use deprecated roles | Minor | cat.aria, wcag2a, wcag412, EN-301-549, EN-9.4.1.2 | failure | [674b10](https://act-rules.github.io/rules/674b10) | diff --git a/lib/checks/aria/braille-label-equivalent-evaluate.js b/lib/checks/aria/braille-label-equivalent-evaluate.js new file mode 100644 index 0000000000..ce301f684b --- /dev/null +++ b/lib/checks/aria/braille-label-equivalent-evaluate.js @@ -0,0 +1,22 @@ +import { sanitize, accessibleTextVirtual } from '../../commons/text'; + +/** + * Check that if aria-braillelabel is not empty, the element has an accessible text + * @memberof checks + * @return {Boolean} + */ +export default function brailleLabelEquivalentEvaluate( + node, + options, + virtualNode +) { + const brailleLabel = virtualNode.attr('aria-braillelabel') ?? ''; + if (!brailleLabel.trim()) { + return true; + } + try { + return sanitize(accessibleTextVirtual(virtualNode)) !== ''; + } catch { + return undefined; + } +} diff --git a/lib/checks/aria/braille-label-equivalent.json b/lib/checks/aria/braille-label-equivalent.json new file mode 100644 index 0000000000..a4d4dd0c1d --- /dev/null +++ b/lib/checks/aria/braille-label-equivalent.json @@ -0,0 +1,12 @@ +{ + "id": "braille-label-equivalent", + "evaluate": "braille-label-equivalent-evaluate", + "metadata": { + "impact": "serious", + "messages": { + "pass": "aria-braillelabel is used on an element with accessible text", + "fail": "aria-braillelabel is used on an element with no accessible text", + "incomplete": "Unable to compute accessible text" + } + } +} diff --git a/lib/checks/aria/braille-roledescription-equivalent-evaluate.js b/lib/checks/aria/braille-roledescription-equivalent-evaluate.js new file mode 100644 index 0000000000..5e03af7fac --- /dev/null +++ b/lib/checks/aria/braille-roledescription-equivalent-evaluate.js @@ -0,0 +1,29 @@ +import { sanitize } from '../../commons/text'; + +/** + * Check that if aria-brailleroledescription is not empty, + * the element has a non-empty aria-roledescription + * @memberof checks + * @return {Boolean} + */ +export default function brailleRoleDescriptionEquivalentEvaluate( + node, + options, + virtualNode +) { + const brailleRoleDesc = virtualNode.attr('aria-brailleroledescription') ?? ''; + if (sanitize(brailleRoleDesc) === '') { + return true; + } + const roleDesc = virtualNode.attr('aria-roledescription'); + if (typeof roleDesc !== 'string') { + this.data({ messageKey: 'noRoleDescription' }); + return false; + } + + if (sanitize(roleDesc) === '') { + this.data({ messageKey: 'emptyRoleDescription' }); + return false; + } + return true; +} diff --git a/lib/checks/aria/braille-roledescription-equivalent.json b/lib/checks/aria/braille-roledescription-equivalent.json new file mode 100644 index 0000000000..f7df5ca9dd --- /dev/null +++ b/lib/checks/aria/braille-roledescription-equivalent.json @@ -0,0 +1,14 @@ +{ + "id": "braille-roledescription-equivalent", + "evaluate": "braille-roledescription-equivalent-evaluate", + "metadata": { + "impact": "serious", + "messages": { + "pass": "aria-brailleroledescription is not used on an element with no accessible text", + "fail": { + "noRoleDescription": "aria-brailleroledescription is used on an element with no aria-roledescription", + "emptyRoleDescription": "aria-brailleroledescription is used on an element with an empty aria-roledescription" + } + } + } +} diff --git a/lib/rules/aria-braille-equivalent.json b/lib/rules/aria-braille-equivalent.json new file mode 100644 index 0000000000..c8509c93e7 --- /dev/null +++ b/lib/rules/aria-braille-equivalent.json @@ -0,0 +1,12 @@ +{ + "id": "aria-braille-equivalent", + "selector": "[aria-brailleroledescription], [aria-braillelabel]", + "tags": ["cat.aria", "wcag2a", "wcag412", "EN-301-549", "EN-9.4.1.2"], + "metadata": { + "description": "Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent", + "help": "aria-braille attributes must have a non-braille equivalent" + }, + "all": ["braille-roledescription-equivalent", "braille-label-equivalent"], + "any": [], + "none": [] +} diff --git a/locales/_template.json b/locales/_template.json index f8358234af..44cc68bef4 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -17,6 +17,10 @@ "description": "Ensures role attribute has an appropriate value for the element", "help": "ARIA role should be appropriate for the element" }, + "aria-braille-equivalent": { + "description": "Ensure aria-braillelabel and aria-brailleroledescription have a non-braille equivalent", + "help": "aria-braille attributes must have a non-braille equivalent" + }, "aria-command-name": { "description": "Ensures every ARIA button, link and menuitem has an accessible name", "help": "ARIA commands must have an accessible name" @@ -541,6 +545,18 @@ "plural": "Invalid ARIA attribute names: ${data.values}" } }, + "braille-label-equivalent": { + "pass": "aria-braillelabel is used on an element with accessible text", + "fail": "aria-braillelabel is used on an element with no accessible text", + "incomplete": "Unable to compute accessible text" + }, + "braille-roledescription-equivalent": { + "pass": "aria-brailleroledescription is not used on an element with no accessible text", + "fail": { + "noRoleDescription": "aria-brailleroledescription is used on an element with no aria-roledescription", + "emptyRoleDescription": "aria-brailleroledescription is used on an element with an empty aria-roledescription" + } + }, "deprecatedrole": { "pass": "ARIA role is not deprecated", "fail": "The role used is deprecated: ${data}" diff --git a/test/checks/aria/braille-label-equivalent.js b/test/checks/aria/braille-label-equivalent.js new file mode 100644 index 0000000000..b98439bc28 --- /dev/null +++ b/test/checks/aria/braille-label-equivalent.js @@ -0,0 +1,51 @@ +describe('braille-label-equivalent tests', () => { + const { checkSetup, getCheckEvaluate } = axe.testUtils; + const checkContext = axe.testUtils.MockCheckContext(); + const checkEvaluate = getCheckEvaluate('braille-label-equivalent'); + + afterEach(() => { + checkContext.reset(); + }); + + it('returns true without aria-braillelabel', () => { + const params = checkSetup(''); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-braillelabel is empty', () => { + const params = checkSetup( + '' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-braillelabel is whitespace-only', () => { + const params = checkSetup( + '' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + describe('when aria-braillelabel has text', () => { + it('returns false when the accessible name is empty', () => { + const params = checkSetup(` + + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('returns false when the accessible name has only whitespace', () => { + const params = checkSetup(` +  \r\t\n + `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when the accessible name is not empty', () => { + const params = checkSetup(` + foo + `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + }); +}); diff --git a/test/checks/aria/braille-roledescription-equivalent.js b/test/checks/aria/braille-roledescription-equivalent.js new file mode 100644 index 0000000000..3dda24c32d --- /dev/null +++ b/test/checks/aria/braille-roledescription-equivalent.js @@ -0,0 +1,80 @@ +describe('braille-roledescription-equivalent tests', () => { + const { checkSetup, getCheckEvaluate } = axe.testUtils; + const checkContext = axe.testUtils.MockCheckContext(); + const checkEvaluate = getCheckEvaluate('braille-roledescription-equivalent'); + + afterEach(() => { + checkContext.reset(); + }); + + it('returns true without aria-brailleroledescription', () => { + const params = checkSetup('
'); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-brailleroledecription is empty', () => { + const params = checkSetup( + '
' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + it('returns true when aria-brailleroledecription is whitespace-only', () => { + const params = checkSetup( + '
' + ); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + + describe('when aria-brailleroledescription has text', () => { + it('returns false without aria-roledescription', () => { + const params = checkSetup(` +
+ `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { messageKey: 'noRoleDescription' }); + }); + + it('returns false when aria-roledescription is empty', () => { + const params = checkSetup(` +
+ `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { + messageKey: 'emptyRoleDescription' + }); + }); + + it('returns false when aria-roledescription has only whitespace', () => { + const params = checkSetup(` +
+ `); + assert.isFalse(checkEvaluate.apply(checkContext, params)); + assert.deepEqual(checkContext._data, { + messageKey: 'emptyRoleDescription' + }); + }); + + it('returns true when aria-roledescription is not empty', () => { + const params = checkSetup(` +
+ `); + assert.isTrue(checkEvaluate.apply(checkContext, params)); + }); + }); +}); diff --git a/test/integration/full/all-rules/all-rules.html b/test/integration/full/all-rules/all-rules.html index ec5a5af5c1..631b2d2e32 100644 --- a/test/integration/full/all-rules/all-rules.html +++ b/test/integration/full/all-rules/all-rules.html @@ -68,7 +68,7 @@

Ok

- + Ok - + Hello + + + + + + + + + + diff --git a/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json new file mode 100644 index 0000000000..725223482e --- /dev/null +++ b/test/integration/rules/aria-braille-equivalent/aria-braille-equivalent.json @@ -0,0 +1,6 @@ +{ + "description": "aria-braille-equivalent tests", + "rule": "aria-braille-equivalent", + "passes": [["#pass1"], ["#pass2"], ["#pass3"], ["#pass4"], ["#pass5"]], + "violations": [["#fail1"], ["#fail2"]] +} diff --git a/test/integration/virtual-rules/aria-braille-equivalent.js b/test/integration/virtual-rules/aria-braille-equivalent.js new file mode 100644 index 0000000000..481697f0e9 --- /dev/null +++ b/test/integration/virtual-rules/aria-braille-equivalent.js @@ -0,0 +1,74 @@ +describe('aria-braille-equivalent virtual-rule', () => { + afterEach(() => { + axe.reset(); + }); + + it('passes when aria-braillelabel is not empty', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'img', + attributes: { + alt: 'Hello world', + 'aria-braillelabel': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('fails when accessible text is empty but braille label is not', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'img', + attributes: { + alt: '', + 'aria-braillelabel': 'hello world' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('passes when roledescription and brailleroledescription are not empty', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'div', + attributes: { + 'aria-roledescription': 'Hello world', + 'aria-brailleroledescription': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('fails when roledescription is empty but brailleroledescription is not', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'div', + attributes: { + 'aria-roledescription': '', + 'aria-brailleroledescription': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('incompletes if the subtree fails to compute with aria-braillelabel', () => { + const results = axe.runVirtualRule('aria-braille-equivalent', { + nodeName: 'button', + attributes: { + 'aria-braillelabel': 'Hello world' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); +});