Skip to content
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
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export { FocusLostRule } from "./rules/focuslost";
export { BadFocusRule } from "./rules/badfocus";
export { FindElementRule } from "./rules/find";
export { CustomNotifyRule } from "./rules/notify";
export { AriaHiddenFocusableRule } from "./rules/ariaHidden";
export {
isAccessibilityAffectingElement,
hasAccessibilityAttribute,
Expand Down
129 changes: 129 additions & 0 deletions src/rules/ariaHidden.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { focusableElementSelector, matchesSelector } from "../utils";
import { ValidationRule, ValidationResult, ValidationRuleType } from "./base";

export class AriaHiddenFocusableRule extends ValidationRule {
type = ValidationRuleType.Error;
name = "AriaHiddenFocusableRule";
anchored = true;

private _isAriaHidden(element: HTMLElement): boolean {
let current: HTMLElement | null = element;

while (current && current !== element.ownerDocument.documentElement) {
if (current.getAttribute("aria-hidden") === "true") {
return true;
}
current = current.parentElement;
}

return false;
}

private _hasAriaHiddenAncestor(element: HTMLElement): boolean {
let current: HTMLElement | null = element.parentElement;

while (current && current !== element.ownerDocument.documentElement) {
if (current.getAttribute("aria-hidden") === "true") {
return true;
}
current = current.parentElement;
}

return false;
}

private _isFocusable(element: HTMLElement): boolean {
if (!matchesSelector(element, focusableElementSelector)) {
return false;
}

const tabindex = element.getAttribute("tabindex");

if (tabindex && parseInt(tabindex, 10) < -1) {
return false;
}

if (element.hasAttribute("disabled")) {
return false;
}

return true;
}

private _findFocusableDescendants(element: HTMLElement): HTMLElement[] {
const focusable: HTMLElement[] = [];
const descendants = element.querySelectorAll(focusableElementSelector);

descendants.forEach((descendant) => {
if (this._isFocusable(descendant as HTMLElement)) {
focusable.push(descendant as HTMLElement);
}
});

return focusable;
}

accept(element: HTMLElement): boolean {
return (
element.getAttribute("aria-hidden") === "true" ||
matchesSelector(element, focusableElementSelector)
);
}

validate(element: HTMLElement): ValidationResult | null {
if (element.getAttribute("aria-hidden") === "true") {
const hasAriaHiddenAncestor = this._hasAriaHiddenAncestor(element);
if (this._isFocusable(element) && !hasAriaHiddenAncestor) {
return {
issue: {
id: "aria-hidden-focusable",
message: "Element with aria-hidden='true' should not be focusable.",
element,
help: "https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap.html",
},
};
}

if (!hasAriaHiddenAncestor) {
const focusableDescendants = this._findFocusableDescendants(element);

if (focusableDescendants.length > 0) {
return {
issue: {
id: "aria-hidden-contains-focusable",
message: `Element with aria-hidden='true' contains ${focusableDescendants.length} focusable ${
focusableDescendants.length === 1 ? "element" : "elements"
}.`,
element,
rel: focusableDescendants[0],
help: "https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap.html",
},
};
}
}
}

if (
this._isFocusable(element) &&
this._isAriaHidden(element) &&
element.getAttribute("aria-hidden") !== "true"
) {
return {
issue: {
id: "focusable-in-aria-hidden",
message:
"Focusable element is inside a container with aria-hidden='true'.",
element,
help: "https://www.w3.org/WAI/WCAG21/Understanding/no-keyboard-trap.html",
},
};
}

return null;
}
}
17 changes: 17 additions & 0 deletions tests/ariaHidden/ariaHidden.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test Page 1</title>
</head>
<body>
<h1>Test Page 1</h1>
<script type="module" src="./ariaHidden.ts"></script>

<i aria-hidden="true">No</i>
<i aria-hidden="true">
<button aria-hidden="true">Hidden Button</button>
</i>
<i aria-hidden="true" tabindex="0">Text</i>
</body>
</html>
23 changes: 23 additions & 0 deletions tests/ariaHidden/ariaHidden.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { AbleDOM, AriaHiddenFocusableRule } from "abledom";

const ableDOM = new AbleDOM(window, {
bugReport: {
isVisible: (issue) => {
return issue.id === "focusable-element-label";
},
onClick: (issue) => {
alert(issue.id);
},
getTitle(issue) {
return `Custom report bug button title for ${issue.id}`;
},
},
});
ableDOM.addRule(new AriaHiddenFocusableRule());

ableDOM.start();