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