diff --git a/README.md b/README.md index b64c0a7..c204cf6 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Rules that do not necessarily conform to WCAG success criterion but are industry | :---------- | :---------------------------------- | :-------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | :----------------- | :---------------------------------------------- | :------------------------- | :------------------------------------------------- | | ❌ | accesskeys | https://dequeuniversity.com/rules/axe/4.4/accesskeys?application=RuleDescription | Ensures every accesskey attribute value is unique | Serious | cat.keyboard, best-practice | failure | | | ❌ | aria-allowed-role | https://dequeuniversity.com/rules/axe/4.4/aria-allowed-role?application=RuleDescription | Ensures role attribute has an appropriate value for the element | Minor | cat.aria, best-practice | failure, needs review | | -| ❌ | aria-dialog-name | https://dequeuniversity.com/rules/axe/4.4/aria-dialog-name?application=RuleDescription | Ensures every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | +| ✅ | aria-dialog-name | https://dequeuniversity.com/rules/axe/4.4/aria-dialog-name?application=RuleDescription | Ensures every ARIA dialog and alertdialog node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | | ❌ | aria-text | https://dequeuniversity.com/rules/axe/4.4/aria-text?application=RuleDescription | Ensures "role=text" is used on elements with no focusable descendants | Serious | cat.aria, best-practice | failure, needs review | | | ❌ | aria-treeitem-name | https://dequeuniversity.com/rules/axe/4.4/aria-treeitem-name?application=RuleDescription | Ensures every ARIA treeitem node has an accessible name | Serious | cat.aria, best-practice | failure, needs review | | | ❌ | empty-heading | https://dequeuniversity.com/rules/axe/4.4/empty-heading?application=RuleDescription | Ensures headings have discernible text | Minor | cat.name-role-value, best-practice | failure, needs review | [ffd0e9](https://act-rules.github.io/rules/ffd0e9) | diff --git a/src/rules/aria-dialog-name.ts b/src/rules/aria-dialog-name.ts new file mode 100644 index 0000000..0d37090 --- /dev/null +++ b/src/rules/aria-dialog-name.ts @@ -0,0 +1,30 @@ +import { AccessibilityError } from "../scanner"; +import { hasAccessibleText, querySelectorAll } from "../utils"; + +// Metadata +const id = "aria-dialog-name"; +const text = "ARIA dialog and alertdialog must have an accessible name"; +const url = `https://dequeuniversity.com/rules/axe/4.4/${id}?application=RuleDescription`; + +/** + * Ensure that each element with role="dialog" or role="alertdialog" has one of the following characteristics: + * + * - Non-empty aria-label attribute. + * - aria-labelledby pointing to element with text which is discernible to screen reader users. + */ +export function ariaDialogName(el: Element): AccessibilityError[] { + const errors = []; + const selector = "[role=dialog],[role=alertdialog]"; + const dialogs = querySelectorAll(selector, el); + if (el.matches(selector)) dialogs.push(el); + for (const dialog of dialogs) { + if (!hasAccessibleText(dialog)) { + errors.push({ + element: dialog, + url, + text, + }); + } + } + return errors; +} diff --git a/src/rules/aria-tooltip-name.ts b/src/rules/aria-tooltip-name.ts index a2ee52a..c9e3e83 100644 --- a/src/rules/aria-tooltip-name.ts +++ b/src/rules/aria-tooltip-name.ts @@ -1,36 +1,11 @@ import { AccessibilityError } from "../scanner"; -import { labelledByIsValid, querySelectorAll } from "../utils"; +import { hasAccessibleText, querySelectorAll } from "../utils"; // Metadata const id = "aria-tooltip-name"; const text = "ARIA tooltip must have an accessible name"; const url = `https://dequeuniversity.com/rules/axe/4.4/${id}?application=RuleDescription`; -/** - * 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. - */ -function hasAccessibleText(el: Element): boolean { - if (el.hasAttribute("aria-label")) { - return el.getAttribute("aria-label")!.trim() !== ""; - } - - if (!labelledByIsValid(el)) return false; - - if (el.getAttribute("title")) { - return el.getAttribute("title")!.trim() !== ""; - } - - if (el.textContent) { - return el.textContent.trim() !== ""; - } - - return true; -} - export function ariaTooltipName(el: Element): AccessibilityError[] { const errors = []; const tooltips = querySelectorAll("[role=tooltip]", el); diff --git a/src/utils.ts b/src/utils.ts index c400466..4f9efa4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,31 @@ 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 (!labelledByIsValid(el)) return false; + + if (el.getAttribute("title")) { + return el.getAttribute("title")!.trim() !== ""; + } + + if (el.textContent) { + return el.textContent.trim() !== ""; + } + + return true; +} + /** * 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-dialog-name.ts b/tests/aria-dialog-name.ts new file mode 100644 index 0000000..ed76cc8 --- /dev/null +++ b/tests/aria-dialog-name.ts @@ -0,0 +1,53 @@ +import { fixture, expect } from "@open-wc/testing"; +import { Scanner } from "../src/scanner"; +import { ariaDialogName } from "../src/rules/aria-dialog-name"; + +const scanner = new Scanner([ariaDialogName]); + +const passes = [ + `