Skip to content

Commit

Permalink
Implement aria-command-name
Browse files Browse the repository at this point in the history
  • Loading branch information
koddsson committed Jul 9, 2024
1 parent d9f7ba4 commit 6ae6b30
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/rules/aria-command-name.ts
Original file line number Diff line number Diff line change
@@ -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";

/*
<div role="link" id="al" aria-label="Name"></div>
<div role="button" id="alb" aria-labelledby="labeldiv"></div>
<div role="menuitem" id="combo" aria-label="Aria Name">Name</div>
<div role="link" id="title" title="Title"></div>
*/

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;
}
23 changes: 23 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions tests/aria-command-name.ts
Original file line number Diff line number Diff line change
@@ -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 = [
`<div role="link" id="al" aria-label="Name"></div>`,
`<div>
<div id="labeldiv">Name</div>
<div role="button" id="alb" aria-labelledby="labeldiv"></div>
</div>`,
`<div role="menuitem" id="combo" aria-label="Aria Name">Name</div>`,
`<div role="link" id="title" title="Title"></div>
`,
];

const violations = [
`<div role="link" id="empty"></div>`,
`<div role="button" id="alempty" aria-label=""></div>`,
`<div role="menuitem" id="albmissing" aria-labelledby="nonexistent"></div>`,
`<div><div role="link" id="albempty" aria-labelledby="emptydiv"></div>
<div id="emptydiv"></div></div>`,
];

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",
},
]);
});
}
});

0 comments on commit 6ae6b30

Please sign in to comment.