a11y: Add role-has-required-aria-props rule (#5852)

* Check required props for ARIA roles

* Test required ARIA props check

* Properly indent with tabs in test

* swtich to use aria-query

* fix validation test

* update docs

Co-authored-by: tanhauhau <lhtan93@gmail.com>
pull/7628/head
Mel 3 years ago committed by GitHub
parent c01dc62314
commit 1d19aebe38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

29
package-lock.json generated

@ -18,12 +18,14 @@
"@rollup/plugin-typescript": "^2.0.1", "@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^2.0.0", "@rollup/plugin-virtual": "^2.0.0",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0", "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0",
"@types/aria-query": "^5.0.0",
"@types/mocha": "^7.0.0", "@types/mocha": "^7.0.0",
"@types/node": "^8.10.53", "@types/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.22.0",
"acorn": "^8.4.1", "acorn": "^8.4.1",
"agadoo": "^1.1.0", "agadoo": "^1.1.0",
"aria-query": "^5.0.0",
"code-red": "^0.2.5", "code-red": "^0.2.5",
"css-tree": "^1.1.2", "css-tree": "^1.1.2",
"eslint": "^8.0.0", "eslint": "^8.0.0",
@ -285,6 +287,12 @@
"resolved": "git+ssh://git@github.com/sveltejs/eslint-config.git#31fd4faeea88990069502460b023698b1c9c2d13", "resolved": "git+ssh://git@github.com/sveltejs/eslint-config.git#31fd4faeea88990069502460b023698b1c9c2d13",
"dev": true "dev": true
}, },
"node_modules/@types/aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-P+dkdFu0n08PDIvw+9nT9ByQnd+Udc8DaWPb9HKfaPwCvWvQpC5XaMRx2xLWECm9x1VKNps6vEAlirjA6+uNrQ==",
"dev": true
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "0.0.50", "version": "0.0.50",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
@ -698,6 +706,15 @@
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"node_modules/aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
"dev": true,
"engines": {
"node": ">=6.0"
}
},
"node_modules/array-equal": { "node_modules/array-equal": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
@ -4927,6 +4944,12 @@
"dev": true, "dev": true,
"from": "@sveltejs/eslint-config@github:sveltejs/eslint-config#v5.8.0" "from": "@sveltejs/eslint-config@github:sveltejs/eslint-config#v5.8.0"
}, },
"@types/aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-P+dkdFu0n08PDIvw+9nT9ByQnd+Udc8DaWPb9HKfaPwCvWvQpC5XaMRx2xLWECm9x1VKNps6vEAlirjA6+uNrQ==",
"dev": true
},
"@types/estree": { "@types/estree": {
"version": "0.0.50", "version": "0.0.50",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
@ -5208,6 +5231,12 @@
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
"dev": true
},
"array-equal": { "array-equal": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",

@ -124,12 +124,14 @@
"@rollup/plugin-typescript": "^2.0.1", "@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^2.0.0", "@rollup/plugin-virtual": "^2.0.0",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0", "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0",
"@types/aria-query": "^5.0.0",
"@types/mocha": "^7.0.0", "@types/mocha": "^7.0.0",
"@types/node": "^8.10.53", "@types/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.22.0",
"acorn": "^8.4.1", "acorn": "^8.4.1",
"agadoo": "^1.1.0", "agadoo": "^1.1.0",
"aria-query": "^5.0.0",
"code-red": "^0.2.5", "code-red": "^0.2.5",
"css-tree": "^1.1.2", "css-tree": "^1.1.2",
"eslint": "^8.0.0", "eslint": "^8.0.0",

@ -54,6 +54,17 @@ The following elements are visually distracting: `<marquee>` and `<blink>`.
--- ---
### `role-has-required-aria-props`
Elements with ARIA roles must have all required attributes for that role.
```sv
<!-- A11y: A11y: Elements with the ARIA role "checkbox" must have the following attributes defined: "aria-checked" -->
<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>
```
---
### `a11y-hidden` ### `a11y-hidden`
Certain DOM elements are useful for screen reader navigation and should not be hidden. Certain DOM elements are useful for screen reader navigation and should not be hidden.

@ -80,6 +80,10 @@ export default {
code: 'a11y-no-redundant-roles', code: 'a11y-no-redundant-roles',
message: `A11y: Redundant role '${role}'` message: `A11y: Redundant role '${role}'`
}), }),
a11y_role_has_required_aria_props: (role: string, props: string[]) => ({
code: 'a11y-role-has-required-aria-props',
message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}`
}),
a11y_accesskey: { a11y_accesskey: {
code: 'a11y-accesskey', code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey' message: 'A11y: Avoid using accesskey'

@ -23,6 +23,7 @@ import { string_literal } from '../utils/stringify';
import { Literal } from 'estree'; import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
import { ARIARoleDefintionKey, roles } from 'aria-query';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/; const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
@ -407,9 +408,9 @@ export default class Element extends Node {
} }
validate_attributes_a11y() { validate_attributes_a11y() {
const { component } = this; const { component, attributes } = this;
this.attributes.forEach(attribute => { attributes.forEach(attribute => {
if (attribute.is_spread) return; if (attribute.is_spread) return;
const name = attribute.name.toLowerCase(); const name = attribute.name.toLowerCase();
@ -462,6 +463,17 @@ export default class Element extends Node {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value)); component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
} }
} }
// role-has-required-aria-props
const role = roles.get(value as ARIARoleDefintionKey);
if (role) {
const required_role_props = Object.keys(role.requiredProps);
const has_missing_props = required_role_props.some(prop => !attributes.find(a => a.name === prop));
if (has_missing_props) {
component.warn(attribute, compiler_warnings.a11y_role_has_required_aria_props(value as string, required_role_props));
}
}
} }
// no-access-key // no-access-key

@ -224,6 +224,21 @@
"line": 15 "line": 15
} }
}, },
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 383,
"column": 18,
"line": 15
},
"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
"pos": 369,
"start": {
"character": 369,
"column": 4,
"line": 15
}
},
{ {
"code": "a11y-no-redundant-roles", "code": "a11y-no-redundant-roles",
"end": { "end": {
@ -239,6 +254,21 @@
"line": 16 "line": 16
} }
}, },
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
"pos": 401,
"start": {
"character": 401,
"column": 4,
"line": 16
},
"end": {
"character": 415,
"column": 18,
"line": 16
}
},
{ {
"code": "a11y-no-redundant-roles", "code": "a11y-no-redundant-roles",
"end": { "end": {
@ -254,6 +284,21 @@
"line": 17 "line": 17
} }
}, },
{
"code": "a11y-role-has-required-aria-props",
"end": {
"character": 447,
"column": 18,
"line": 17
},
"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
"pos": 433,
"start": {
"character": 433,
"column": 4,
"line": 17
}
},
{ {
"code": "a11y-no-redundant-roles", "code": "a11y-no-redundant-roles",
"end": { "end": {
@ -269,6 +314,21 @@
"line": 18 "line": 18
} }
}, },
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
"pos": 465,
"start": {
"character": 465,
"column": 4,
"line": 18
},
"end": {
"character": 479,
"column": 18,
"line": 18
}
},
{ {
"code": "a11y-no-redundant-roles", "code": "a11y-no-redundant-roles",
"end": { "end": {
@ -284,6 +344,21 @@
"line": 19 "line": 19
} }
}, },
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
"pos": 497,
"start": {
"character": 497,
"column": 4,
"line": 19
},
"end": {
"character": 511,
"column": 18,
"line": 19
}
},
{ {
"code": "a11y-no-redundant-roles", "code": "a11y-no-redundant-roles",
"end": { "end": {
@ -299,6 +374,21 @@
"line": 20 "line": 20
} }
}, },
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
"pos": 529,
"start": {
"character": 529,
"column": 4,
"line": 20
},
"end": {
"character": 543,
"column": 18,
"line": 20
}
},
{ {
"code": "a11y-no-redundant-roles", "code": "a11y-no-redundant-roles",
"end": { "end": {
@ -434,6 +524,21 @@
"line": 29 "line": 29
} }
}, },
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"option\" must have the following attributes defined: \"aria-selected\"",
"pos": 751,
"start": {
"character": 751,
"column": 8,
"line": 29
},
"end": {
"character": 764,
"column": 21,
"line": 29
}
},
{ {
"code": "a11y-no-redundant-roles", "code": "a11y-no-redundant-roles",
"end": { "end": {

@ -0,0 +1,9 @@
<div role="heading"></div>
<span role="checkbox"></span>
<div role="meter"></div>
<div role="scrollbar"></div>
<div role="heading" aria-level="1"></div>
<span role="checkbox" aria-checked="false"></span>
<div role="meter" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
<div role="scrollbar" aria-controls="panel" aria-valuenow="50"></div>

@ -0,0 +1,62 @@
[
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 19,
"character": 19
},
"pos": 5
},
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"checkbox\" must have the following attributes defined: \"aria-checked\"",
"start": {
"line": 2,
"column": 6,
"character": 33
},
"end": {
"line": 2,
"column": 21,
"character": 48
},
"pos": 33
},
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"meter\" must have the following attributes defined: \"aria-valuenow\"",
"start": {
"line": 3,
"column": 5,
"character": 62
},
"end": {
"line": 3,
"column": 17,
"character": 74
},
"pos": 62
},
{
"code": "a11y-role-has-required-aria-props",
"message": "A11y: Elements with the ARIA role \"scrollbar\" must have the following attributes defined: \"aria-controls\", \"aria-valuenow\"",
"start": {
"character": 87,
"column": 5,
"line": 4
},
"end": {
"character": 103,
"column": 21,
"line": 4
},
"pos": 87
}
]
Loading…
Cancel
Save