a11y validation - role-has-required-aria-props, a11y-role-supports-aria-props

pull/3725/head
Tan Li Hau 6 years ago
parent 34e831b88d
commit 4b67ea9562

9
package-lock.json generated

@ -358,6 +358,15 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"axobject-query": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz",
"integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==",
"dev": true,
"requires": {
"ast-types-flow": "0.0.7"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",

@ -63,6 +63,7 @@
"acorn": "^7.0.0",
"agadoo": "^1.1.0",
"aria-query": "^3.0.0",
"axobject-query": "^2.0.2",
"c8": "^5.0.1",
"code-red": "0.0.17",
"codecov": "^3.5.0",

@ -5,8 +5,9 @@ import fuzzymatch from '../../../utils/fuzzymatch';
import emojiRegex from 'emoji-regex';
import Text from '../../nodes/Text';
import { array_to_string, is_hidden_from_screen_reader } from './utils';
import { roles } from 'aria-query';
import { roles, aria } from 'aria-query';
import get_implicit_role from './implicit_role';
import is_semantic_role_element from './is_semantic_role_element';
export default function validateA11y(element: Element) {
const attribute_map = new Map();
@ -26,7 +27,7 @@ export default function validateA11y(element: Element) {
no_missing_handlers(element, handler_map);
img_redundant_alt(element, attribute_map);
accessible_emoji(element, attribute_map);
no_unknown_role(element, attribute_map);
validate_aria_role(element, attribute_map);
}
const a11y_distracting_elements = new Set(['blink', 'marquee']);
@ -263,43 +264,83 @@ function contain_text(node, regex: RegExp) {
}
}
const aria_role_set = new Set(roles.keys());
const aria_roles = [...aria_role_set];
const aria_roles = [...roles.keys()];
const role_exceptions = new Map([['nav', 'navigation']]);
function no_unknown_role(
function validate_aria_role(
element: Element,
attribute_map: Map<string, Attribute>
) {
if (!attribute_map.has('role')) {
const role_attribute = attribute_map.get('role');
const explicit_role_name = role_attribute ? role_attribute.get_static_value() : '';
const implicit_role_name = get_implicit_role(element.name, attribute_map);
const role_name = explicit_role_name
? explicit_role_name
: implicit_role_name;
if (!role_name) {
return;
}
const role_attribute = attribute_map.get('role');
const value = role_attribute.get_static_value();
// @ts-ignore
if (value && !aria_role_set.has(value)) {
if (!roles.has(role_name)) {
// @ts-ignore
const match = fuzzymatch(value, aria_roles);
let message = `A11y: Unknown role '${value}'`;
const match = fuzzymatch(role_name, aria_roles);
let message = `A11y: Unknown role '${role_name}'`;
if (match) message += ` (did you mean '${match}'?)`;
element.component.warn(role_attribute, {
code: `a11y-unknown-role`,
message,
});
return;
}
const is_implicit_role = role_name && !explicit_role_name;
const implicit_message = `This role is implicit on the element "${element.name}".`;
const {
requiredProps: requiredPropKeyValues,
props: propKeyValues,
} = roles.get(role_name);
const requiredProps = Object.keys(requiredPropKeyValues);
if (
!is_semantic_role_element(element.name, attribute_map) &&
requiredProps.length > 0
) {
const hasRequiredProps = requiredProps.every(prop =>
attribute_map.has(prop)
);
if (hasRequiredProps === false) {
element.component.warn(role_attribute || element, {
code: `a11y-role-has-required-aria-props`,
message: `Elements with the ARIA role "${role_name}" must have the following attributes defined: ${String(
requiredProps
).toLowerCase()}.${is_implicit_role ? '\n' + implicit_message : ''}`,
});
}
}
for (const [attribute_name, attribute] of attribute_map) {
if (aria.has(attribute_name) && !(attribute_name in propKeyValues)) {
element.component.warn(attribute, {
code: `a11y-role-supports-aria-props`,
message: `The attribute ${attribute_name} is not supported by the role "${role_name}".${is_implicit_role ? '\n' + implicit_message : ''}`,
});
}
}
const implicit_role = get_implicit_role(element.name, attribute_map);
if (implicit_role && implicit_role === value) {
if (implicit_role_name && implicit_role_name === explicit_role_name) {
if (
!(
role_exceptions.has(element.name) &&
role_exceptions.get(element.name) === value
role_exceptions.get(element.name) === role_name
)
) {
element.component.warn(role_attribute, {
code: `a11y-redundant-role`,
message: `The element '${element.name}' has an implicit role of '${implicit_role}'. Defining this explicitly is redundant and should be avoided.`,
message: `The element '${element.name}' has an implicit role of '${implicit_role_name}'. Defining this explicitly is redundant and should be avoided.`,
});
}
}

@ -18,8 +18,9 @@ function img(attribute_map: Map<string, Attribute>) {
}
function input(attribute_map: Map<string, Attribute>) {
if (attribute_map.has('type')) {
const value = attribute_map.get('type').get_static_value() || '';
switch (value.toUpperCase()) {
const type = attribute_map.get('type').get_static_value();
const value = typeof type === 'string' ? type.toUpperCase() : '';
switch (value) {
case 'BUTTON':
case 'IMAGE':
case 'RESET':

@ -0,0 +1,46 @@
import { AXObjectRoles, elementAXObjects } from 'axobject-query';
import Attribute from '../../nodes/Attribute';
export default (
name: string,
attribute_map: Map<string, Attribute>
): boolean => {
if (!attribute_map.has('role')) {
return false;
}
const value = attribute_map.get('role').get_static_value();
for (const [concept, ax_objects] of elementAXObjects) {
if (
// @ts-ignore
concept.name === name &&
// @ts-ignore
(concept.attributes
? // @ts-ignore
concept.attributes.every(
attribute =>
attribute_map.has(attribute.name) &&
(attribute.value !== undefined
? (console.log(
attribute_map.get(attribute.name).get_static_value(),
attribute.value
),
attribute_map.get(attribute.name).get_static_value()) ===
attribute.value
: true)
)
: true)
) {
for (const ax_object of ax_objects) {
if (AXObjectRoles.has(ax_object)) {
for (const role of AXObjectRoles.get(ax_object)) {
if (role.name === value) {
return true;
}
}
}
}
}
}
return false;
};

@ -0,0 +1,43 @@
<!-- valid -->
<Bar baz />
<MyComponent role="combobox" />
<div />
<div></div>
<div role={role} />
<div role={role || "button"} />
<div role={role || "foobar"} />
<div role="row" />
<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>
<input type="checkbox" role="switch" />
<!-- invalid -->
<!-- slider -->
<div role="slider" />
<div role="slider" aria-valuemax={1} />
<div role="slider" aria-valuemax={1} aria-valuemin={1} />
<div role="slider" aria-valuemax={1} aria-valuenow={1} />
<div role="slider" aria-valuemin={1} aria-valuenow={1} />
<!-- spinbutton -->
<div role="spinbutton" />
<div role="spinbutton" aria-valuemax={1} />
<div role="spinbutton" aria-valuemax={1} aria-valuemin={1} />
<div role="spinbutton" aria-valuemax={1} aria-valuenow={1} />
<div role="spinbutton" aria-valuemin={1} aria-valuenow={1} />
<!-- checkbox -->
<div role="checkbox" />
<div role="checkbox" checked />
<div role="checkbox" aria-chcked />
<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>
<!-- combobox -->
<div role="combobox" />
<div role="combobox" expanded />
<div role="combobox" aria-expandd />
<!-- scrollbar -->
<div role="scrollbar" />
<div role="scrollbar" aria-valuemax={1} />
<div role="scrollbar" aria-valuemax={1} aria-valuemin={1} />
<div role="scrollbar" aria-valuemax={1} aria-valuenow={1} />
<div role="scrollbar" aria-valuemin={1} aria-valuenow={1} />
<script>
let Bar, MyComponent, role, props;
</script>

@ -0,0 +1,362 @@
[
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 359,
"column": 18,
"line": 14
},
"message": "Elements with the ARIA role \"slider\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 346,
"start": {
"character": 346,
"column": 5,
"line": 14
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 381,
"column": 18,
"line": 15
},
"message": "Elements with the ARIA role \"slider\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 368,
"start": {
"character": 368,
"column": 5,
"line": 15
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 421,
"column": 18,
"line": 16
},
"message": "Elements with the ARIA role \"slider\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 408,
"start": {
"character": 408,
"column": 5,
"line": 16
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 479,
"column": 18,
"line": 17
},
"message": "Elements with the ARIA role \"slider\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 466,
"start": {
"character": 466,
"column": 5,
"line": 17
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 537,
"column": 18,
"line": 18
},
"message": "Elements with the ARIA role \"slider\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 524,
"start": {
"character": 524,
"column": 5,
"line": 18
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 619,
"column": 22,
"line": 20
},
"message": "Elements with the ARIA role \"spinbutton\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 602,
"start": {
"character": 602,
"column": 5,
"line": 20
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 645,
"column": 22,
"line": 21
},
"message": "Elements with the ARIA role \"spinbutton\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 628,
"start": {
"character": 628,
"column": 5,
"line": 21
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 689,
"column": 22,
"line": 22
},
"message": "Elements with the ARIA role \"spinbutton\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 672,
"start": {
"character": 672,
"column": 5,
"line": 22
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 751,
"column": 22,
"line": 23
},
"message": "Elements with the ARIA role \"spinbutton\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 734,
"start": {
"character": 734,
"column": 5,
"line": 23
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 813,
"column": 22,
"line": 24
},
"message": "Elements with the ARIA role \"spinbutton\" must have the following attributes defined: aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 796,
"start": {
"character": 796,
"column": 5,
"line": 24
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 891,
"column": 20,
"line": 26
},
"message": "Elements with the ARIA role \"checkbox\" must have the following attributes defined: aria-checked.",
"pos": 876,
"start": {
"character": 876,
"column": 5,
"line": 26
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 915,
"column": 20,
"line": 27
},
"message": "Elements with the ARIA role \"checkbox\" must have the following attributes defined: aria-checked.",
"pos": 900,
"start": {
"character": 900,
"column": 5,
"line": 27
}
},
{
"code": "a11y-unknown-aria-attribute",
"end": {
"character": 959,
"column": 32,
"line": 28
},
"message": "A11y: Unknown aria attribute 'aria-chcked' (did you mean 'aria-checked'?)",
"pos": 948,
"start": {
"character": 948,
"column": 21,
"line": 28
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 947,
"column": 20,
"line": 28
},
"message": "Elements with the ARIA role \"checkbox\" must have the following attributes defined: aria-checked.",
"pos": 932,
"start": {
"character": 932,
"column": 5,
"line": 28
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 984,
"column": 21,
"line": 29
},
"message": "Elements with the ARIA role \"checkbox\" must have the following attributes defined: aria-checked.",
"pos": 969,
"start": {
"character": 969,
"column": 6,
"line": 29
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1066,
"column": 20,
"line": 31
},
"message": "Elements with the ARIA role \"combobox\" must have the following attributes defined: aria-controls,aria-expanded.",
"pos": 1051,
"start": {
"character": 1051,
"column": 5,
"line": 31
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1090,
"column": 20,
"line": 32
},
"message": "Elements with the ARIA role \"combobox\" must have the following attributes defined: aria-controls,aria-expanded.",
"pos": 1075,
"start": {
"character": 1075,
"column": 5,
"line": 32
}
},
{
"code": "a11y-unknown-aria-attribute",
"end": {
"character": 1136,
"column": 33,
"line": 33
},
"message": "A11y: Unknown aria attribute 'aria-expandd' (did you mean 'aria-expanded'?)",
"pos": 1124,
"start": {
"character": 1124,
"column": 21,
"line": 33
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1123,
"column": 20,
"line": 33
},
"message": "Elements with the ARIA role \"combobox\" must have the following attributes defined: aria-controls,aria-expanded.",
"pos": 1108,
"start": {
"character": 1108,
"column": 5,
"line": 33
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1180,
"column": 21,
"line": 35
},
"message": "Elements with the ARIA role \"scrollbar\" must have the following attributes defined: aria-controls,aria-orientation,aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 1164,
"start": {
"character": 1164,
"column": 5,
"line": 35
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1205,
"column": 21,
"line": 36
},
"message": "Elements with the ARIA role \"scrollbar\" must have the following attributes defined: aria-controls,aria-orientation,aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 1189,
"start": {
"character": 1189,
"column": 5,
"line": 36
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1248,
"column": 21,
"line": 37
},
"message": "Elements with the ARIA role \"scrollbar\" must have the following attributes defined: aria-controls,aria-orientation,aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 1232,
"start": {
"character": 1232,
"column": 5,
"line": 37
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1309,
"column": 21,
"line": 38
},
"message": "Elements with the ARIA role \"scrollbar\" must have the following attributes defined: aria-controls,aria-orientation,aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 1293,
"start": {
"character": 1293,
"column": 5,
"line": 38
}
},
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 1370,
"column": 21,
"line": 39
},
"message": "Elements with the ARIA role \"scrollbar\" must have the following attributes defined: aria-controls,aria-orientation,aria-valuemax,aria-valuemin,aria-valuenow.",
"pos": 1354,
"start": {
"character": 1354,
"column": 5,
"line": 39
}
}
]

@ -0,0 +1,5 @@
<a href="foo.bar" aria-checked>anchor</a>
<link href="#" aria-checked />
<img alt="foobar" aria-checked />
<menu type="toolbar" aria-checked />
<aside aria-checked />

@ -0,0 +1,77 @@
[
{
"code": "a11y-role-supports-aria-props",
"end": {
"character": 30,
"column": 30,
"line": 1
},
"message": "The attribute aria-checked is not supported by the role \"link\".\nThis role is implicit on the element \"a\".",
"pos": 18,
"start": {
"character": 18,
"column": 18,
"line": 1
}
},
{
"code": "a11y-role-supports-aria-props",
"end": {
"character": 69,
"column": 27,
"line": 2
},
"message": "The attribute aria-checked is not supported by the role \"link\".\nThis role is implicit on the element \"link\".",
"pos": 57,
"start": {
"character": 57,
"column": 15,
"line": 2
}
},
{
"code": "a11y-role-supports-aria-props",
"end": {
"character": 103,
"column": 30,
"line": 3
},
"message": "The attribute aria-checked is not supported by the role \"img\".\nThis role is implicit on the element \"img\".",
"pos": 91,
"start": {
"character": 91,
"column": 18,
"line": 3
}
},
{
"code": "a11y-role-supports-aria-props",
"end": {
"character": 140,
"column": 33,
"line": 4
},
"message": "The attribute aria-checked is not supported by the role \"toolbar\".\nThis role is implicit on the element \"menu\".",
"pos": 128,
"start": {
"character": 128,
"column": 21,
"line": 4
}
},
{
"code": "a11y-role-supports-aria-props",
"end": {
"character": 163,
"column": 19,
"line": 5
},
"message": "The attribute aria-checked is not supported by the role \"complementary\".\nThis role is implicit on the element \"aside\".",
"pos": 151,
"start": {
"character": 151,
"column": 7,
"line": 5
}
}
]

@ -3,5 +3,5 @@
</script>
<select bind:value multiple>
<option>1</option>
<option aria-selected="false">1</option>
</select>
Loading…
Cancel
Save