Skip to content

Commit

Permalink
feat(rule): Flag div/p/spans/headings in focus order
Browse files Browse the repository at this point in the history
Flag elements inserted into focus order without semantics that signal
interactivity.

fixes #632
  • Loading branch information
0ddfell0w committed Jan 11, 2018
1 parent c665d0b commit ce5f3dc
Show file tree
Hide file tree
Showing 14 changed files with 1,254 additions and 333 deletions.
1 change: 1 addition & 0 deletions doc/rule-descriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
| document-title | Ensures each HTML document contains a non-empty <title> element | cat.text-alternatives, wcag2a, wcag242 | true |
| duplicate-id | Ensures every id attribute value is unique | cat.parsing, wcag2a, wcag411 | true |
| empty-heading | Ensures headings have discernible text | cat.name-role-value, best-practice | true |
| focus-order-semantics | Ensures elements placed in the focus order have an appropriate aria role for interactive content | cat.keyboard, best-practice, experimental | true |
| frame-title-unique | Ensures <iframe> and <frame> elements contain a unique title attribute | cat.text-alternatives, best-practice | true |
| frame-title | Ensures <iframe> and <frame> elements contain a non-empty title attribute | cat.text-alternatives, wcag2a, wcag241, section508, section508.22.i | true |
| heading-order | Ensures the order of headings is semantically correct | cat.semantics, best-practice | true |
Expand Down
6 changes: 6 additions & 0 deletions lib/checks/aria/has-widget-role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var role = node.getAttribute('role');
if (role === null) {
return false;
}
var roleType = axe.commons.aria.getRoleType(role);
return roleType === 'widget' || roleType === 'composite';
12 changes: 12 additions & 0 deletions lib/checks/aria/has-widget-role.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "has-widget-role",
"evaluate": "has-widget-role.js",
"options": [],
"metadata": {
"impact": "minor",
"messages": {
"pass": "Element has a widget role.",
"fail": "Element does not have a widget role."
}
}
}
61 changes: 61 additions & 0 deletions lib/checks/aria/valid-scrollable-semantics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* A map from HTML tag names to a boolean which reflects whether it is
* appropriate for scrollable elements found in the focus order.
*/
const VALID_TAG_NAMES_FOR_SCROLLABLE_REGIONS = {
ARTICLE: true,
ASIDE: true,
NAV: true,
SECTION: true
};

/**
* A map from each landmark role to a boolean which reflects whether it is
* appropriate for scrollable elements found in the focus order.
*/
const VALID_ROLES_FOR_SCROLLABLE_REGIONS = {
banner: false,
complementary: true,
contentinfo: true,
form: true,
main: true,
navigation: true,
region: true,
search: false
};

/**
* @param {HTMLElement} node
* @return {Boolean} Whether the element has a tag appropriate for a scrollable
* region.
*/
function validScrollableTagName(node) {
// Some elements with nonsensical roles will pass this check, but should be
// flagged by other checks.
var tagName = node.tagName.toUpperCase();
return VALID_TAG_NAMES_FOR_SCROLLABLE_REGIONS[tagName] || false;
}

/**
* @param {HTMLElement} node
* @return {Boolean} Whether the node has a role appropriate for a scrollable
* region.
*/
function validScrollableRole(node) {
var role = node.getAttribute('role');
if (!role) {
return false;
}
return VALID_ROLES_FOR_SCROLLABLE_REGIONS[role.toLowerCase()] || false;
}

/**
* @param {HTMLElement} node
* @return {Boolean} Whether the element would have valid semantics if it were a
* scrollable region.
*/
function validScrollableSemantics(node) {
return validScrollableRole(node) || validScrollableTagName(node);
}

return validScrollableSemantics(node);
12 changes: 12 additions & 0 deletions lib/checks/aria/valid-scrollable-semantics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"id": "valid-scrollable-semantics",
"evaluate": "valid-scrollable-semantics.js",
"options": [],
"metadata": {
"impact": "minor",
"messages": {
"pass": "Element has valid semantics for an element in the focus order.",
"fail": "Element has invalid semantics for an element in the focus order."
}
}
}
15 changes: 15 additions & 0 deletions lib/commons/dom/is-focusable.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,18 @@ dom.isNativelyFocusable = function(el) {
}
return false;
};

/**
* Determines if an element is in the focus order, but would not be if its
* tabindex were unspecified.
* @method insertedIntoFocusOrder
* @memberof axe.commons.dom
* @instance
* @param {HTMLElement} el The HTMLElement
* @return {Boolean} True if the element is in the focus order but wouldn't be
* if its tabindex were removed. Else, false.
*/
dom.insertedIntoFocusOrder = function(el) {
return (el.tabIndex > -1 && dom.isFocusable(el) &&
!dom.isNativelyFocusable(el));
};
13 changes: 13 additions & 0 deletions lib/rules/focus-order-semantics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"id": "focus-order-semantics",
"selector": "div, h1, h2, h3, h4, h5, h6, [role=heading], p, span",
"matches": "inserted-into-focus-order-matches.js",
"tags": ["cat.keyboard", "best-practice", "experimental"],
"metadata": {
"description": "Ensures elements in the focus order have an appropriate role",
"help": "Elements in the focus order need a role appropriate for interactive content"
},
"all": [],
"any": ["has-widget-role", "valid-scrollable-semantics"],
"none": []
}
1 change: 1 addition & 0 deletions lib/rules/inserted-into-focus-order-matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
return axe.commons.dom.insertedIntoFocusOrder(node);
Loading

0 comments on commit ce5f3dc

Please sign in to comment.