diff --git a/src/rules/aria-command-name.ts b/src/rules/aria-command-name.ts new file mode 100644 index 0000000..87ea38d --- /dev/null +++ b/src/rules/aria-command-name.ts @@ -0,0 +1,33 @@ +import { AccessibilityError } from "../scanner"; +import { querySelectorAll, hasAccessibleText } from "../utils"; + +const text = "ARIA button, link, and menuitem must have an accessible name"; +const url = "https://dequeuniversity.com/rules/axe/4.4/aria-command-name"; + +/* +
+ +
+ + + +
+*/ + +export default function (el: Element): AccessibilityError[] { + const errors = []; + const elements = querySelectorAll( + '[role="link"], [role="button"], [role="menuitem"]', + el, + ); + + if (el.matches('[role="link"], [role="button"], [role="menuitem"]')) { + elements.push(el as HTMLAudioElement); + } + + for (const element of elements) { + if (hasAccessibleText(element)) continue; + errors.push({ element, text, url }); + } + return errors; +} diff --git a/src/utils.ts b/src/utils.ts index 6dc04a8..4d69211 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,29 @@ export function isVisible(el: HTMLElement): boolean { return el.style.display !== "none"; } +/** + * Make sure that a elements text is "visible" to a screenreader user. + * + * - Inner text that is discernible to screen reader users. + * - Non-empty aria-label attribute. + * - aria-labelledby pointing to element with text which is discernible to screen reader users. + */ +export function hasAccessibleText(el: Element): boolean { + if (el.hasAttribute("aria-label")) { + return el.getAttribute("aria-label")!.trim() !== ""; + } + + if (el.getAttribute("title")) { + return el.getAttribute("title")!.trim() !== ""; + } + + if (el.hasAttribute("aria-labelledby")) { + return labelledByIsValid(el); + } + + return el.textContent?.trim() !== ""; +} + /** * Given a element, make sure that it's `aria-labelledby` has a value and it's * value maps to a element in the DOM that has valid text diff --git a/tests/aria-command-name.ts b/tests/aria-command-name.ts new file mode 100644 index 0000000..18c662a --- /dev/null +++ b/tests/aria-command-name.ts @@ -0,0 +1,53 @@ +import { fixture, expect } from "@open-wc/testing"; +import { Scanner } from "../src/scanner"; +import rule from "../src/rules/aria-command-name"; + +const scanner = new Scanner([rule]); + +const passes = [ + `
`, + `
+
Name
+
+
`, + ``, + `
+`, +]; + +const violations = [ + `
`, + `
`, + ``, + `
+
`, +]; + +describe("aria-command-name", async function () { + for (const markup of passes) { + const el = await fixture(markup); + it(el.outerHTML, async function () { + const results = (await scanner.scan(el)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.be.empty; + }); + } + + for (const markup of violations) { + const el = await fixture(markup); + it(el.outerHTML, async function () { + const results = (await scanner.scan(el)).map(({ text, url }) => { + return { text, url }; + }); + + expect(results).to.eql([ + { + text: "ARIA button, link, and menuitem must have an accessible name", + url: "https://dequeuniversity.com/rules/axe/4.4/aria-command-name", + }, + ]); + }); + } +});