Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aria dialog name #40

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
30 changes: 30 additions & 0 deletions src/rules/aria-dialog-name.ts
Original file line number Diff line number Diff line change
@@ -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;
}
27 changes: 1 addition & 26 deletions src/rules/aria-tooltip-name.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
25 changes: 25 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions tests/aria-dialog-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 { ariaDialogName } from "../src/rules/aria-dialog-name";

const scanner = new Scanner([ariaDialogName]);

const passes = [
`<div>
<div role="dialog" id="alb" aria-labelledby="labeldiv"></div>
<div id="labeldiv">My dialog!</div>
</div>`,
`<div role="alertdialog" id="combo" aria-label="Aria Name">Name</div>`,
`<div role="dialog" id="title" title="Title"></div>`,
];

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

describe("aria-dialog-name", async function () {
for (const markup of passes) {
const el = await fixture(markup);
it(el.outerHTML, async () => {
const results = (await scanner.scan(el)).map(({ text, url }) => {
return { text, url };
});

expect(results).to.be.empty;
});
}

for await (const markup of violations) {
const el = await fixture(markup);
it(el.outerHTML, async () => {
const results = (await scanner.scan(el)).map(({ text, url }) => {
return { text, url };
});

expect(results).to.eql([
{
text: "ARIA tooltip must have an accessible name",
url: "https://dequeuniversity.com/rules/axe/4.4/aria-tooltip-name?application=RuleDescription",
},
]);
});
}
});
39 changes: 20 additions & 19 deletions web-test-runner.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,12 @@ import { junitReporter } from "@web/test-runner-junit-reporter";

const browsers = [playwrightLauncher({ product: "chromium" })];

if (env.CI) {
browsers.push(
playwrightLauncher({ product: "firefox" }),
playwrightLauncher({ product: "webkit" }),
);
}

const reporters = [
summaryReporter(),
env.CI
? junitReporter({
outputPath: "./test-results.xml",
reportLogs: true,
})
: null,
];

export default {
const config = {
nodeResolve: true,
coverage: true,
files: ["tests/**/*.ts", "tests/**/*.js"],
plugins: [esbuildPlugin({ ts: true, target: "esnext" })],
browsers,
reporters,
filterBrowserLogs(log) {
if (
typeof log.args[0] === "string" &&
Expand All @@ -44,3 +26,22 @@ export default {
return true;
},
};

if (env.CI) {
config.browsers.push(
playwrightLauncher({ product: "firefox" }),
playwrightLauncher({ product: "webkit" }),
);

config.reporters = [
summaryReporter(),
env.CI
? junitReporter({
outputPath: "./test-results.xml",
reportLogs: true,
})
: null,
];
}

export default config;
Loading