[feat]: Add A11y aria-proptypes check (#6978)

* Add aria prop type list

* feat: check aria attribute types

* feat: add proptype tests

* Add documentation

* use aria-query

Co-authored-by: Nurassyl Zekenov <nurassyl@snoonu.com>
Co-authored-by: mka_882@yahoo.com <mka_882@yahoo.com>
Co-authored-by: tanhauhau <lhtan93@gmail.com>
Co-authored-by: David Mosher <davidmosher@gmail.com>
pull/5613/head
kwangure 2 years ago committed by GitHub
parent 4617c0d5f5
commit 39901986d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -98,6 +98,18 @@ Enforce img alt attribute does not contain the word image, picture, or photo. Sc
--- ---
### `a11y-incorrect-aria-attribute-type`
Enforce that only the correct type of value is used for aria attributes. For example, `aria-hidden`
should only receive a boolean.
```sv
<!-- A11y: The value of 'aria-hidden' must be exactly one of true or false -->
<div aria-hidden="yes"/>
```
---
### `a11y-invalid-attribute` ### `a11y-invalid-attribute`
Enforce that attributes important for accessibility have a valid value. For example, `href` should not be empty, `'#'`, or `javascript:`. Enforce that attributes important for accessibility have a valid value. For example, `href` should not be empty, `'#'`, or `javascript:`.

@ -1,5 +1,7 @@
// All compiler warnings should be listed and accessed from here // All compiler warnings should be listed and accessed from here
import { ARIAPropertyDefinition } from 'aria-query';
/** /**
* @internal * @internal
*/ */
@ -60,6 +62,35 @@ export default {
code: 'a11y-aria-attributes', code: 'a11y-aria-attributes',
message: `A11y: <${name}> should not have aria-* attributes` message: `A11y: <${name}> should not have aria-* attributes`
}), }),
a11y_incorrect_attribute_type: (schema: ARIAPropertyDefinition, attribute: string) => {
let message;
switch (schema.type) {
case 'boolean':
message = `The value of '${attribute}' must be exactly one of true or false`;
break;
case 'id':
message = `The value of '${attribute}' must be a string that represents a DOM element ID`;
break;
case 'idlist':
message = `The value of '${attribute}' must be a space-separated list of strings that represent DOM element IDs`;
break;
case 'tristate':
message = `The value of '${attribute}' must be exactly one of true, false, or mixed`;
break;
case 'token':
message = `The value of '${attribute}' must be exactly one of ${(schema.values || []).join(', ')}`;
break;
case 'tokenlist':
message = `The value of '${attribute}' must be a space-separated list of one or more of ${(schema.values || []).join(', ')}`;
break;
default:
message = `The value of '${attribute}' must be of type ${schema.type}`;
}
return {
code: 'a11y-incorrect-aria-attribute-type',
message: `A11y: ${message}`
};
},
a11y_unknown_aria_attribute: (attribute: string, suggestion?: string) => ({ a11y_unknown_aria_attribute: (attribute: string, suggestion?: string) => ({
code: 'a11y-unknown-aria-attribute', code: 'a11y-unknown-aria-attribute',
message: `A11y: Unknown aria attribute 'aria-${attribute}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '') message: `A11y: Unknown aria attribute 'aria-${attribute}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')

@ -23,7 +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'; import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } 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)$/;
@ -177,6 +177,32 @@ function get_namespace(parent: Element, element: Element, explicit_namespace: st
return parent_element.namespace; return parent_element.namespace;
} }
function is_valid_aria_attribute_value(schema: ARIAPropertyDefinition, value: string | boolean): boolean {
switch (schema.type) {
case 'boolean':
return typeof value === 'boolean';
case 'string':
case 'id':
return typeof value === 'string';
case 'tristate':
return typeof value === 'boolean' || value === 'mixed';
case 'integer':
case 'number':
return typeof value !== 'boolean' && isNaN(Number(value)) === false;
case 'token': // single token
return (schema.values || [])
.indexOf(typeof value === 'string' ? value.toLowerCase() : value) > -1;
case 'idlist': // if list of ids, split each
return typeof value === 'string'
&& value.split(' ').every((id) => typeof id === 'string');
case 'tokenlist': // if list of tokens, split each
return typeof value === 'string'
&& value.split(' ').every((token) => (schema.values || []).indexOf(token.toLowerCase()) > -1);
default:
return false;
}
}
export default class Element extends Node { export default class Element extends Node {
type: 'Element'; type: 'Element';
name: string; name: string;
@ -431,6 +457,18 @@ export default class Element extends Node {
if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) { if (name === 'aria-hidden' && /^h[1-6]$/.test(this.name)) {
component.warn(attribute, compiler_warnings.a11y_hidden(this.name)); component.warn(attribute, compiler_warnings.a11y_hidden(this.name));
} }
// aria-proptypes
let value = attribute.get_static_value();
if (value === 'true') value = true;
if (value === 'false') value = false;
if (value !== null && value !== undefined && aria.has(name as ARIAProperty)) {
const schema = aria.get(name as ARIAProperty);
if (!is_valid_aria_attribute_value(schema, value)) {
component.warn(attribute, compiler_warnings.a11y_incorrect_attribute_type(schema, name));
}
}
} }
// aria-role // aria-role

@ -0,0 +1,8 @@
<script>
const abc = 'abc';
</script>
<button aria-disabled="yes"/>
<button aria-disabled="no"/>
<button aria-disabled={1234}/>
<button aria-disabled={`${abc}`}/>

@ -0,0 +1,32 @@
[
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-disabled' must be exactly one of true or false",
"start": {
"line": 5,
"column": 8,
"character": 51
},
"end": {
"line": 5,
"column": 27,
"character": 70
},
"pos": 51
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-disabled' must be exactly one of true or false",
"start": {
"line": 6,
"column": 8,
"character": 81
},
"end": {
"line": 6,
"column": 26,
"character": 99
},
"pos": 81
}
]

@ -0,0 +1,7 @@
<div aria-level="yes" />
<div aria-level="no" />
<div aria-level={`abc`} />
<div aria-level={true} />
<div aria-level />
<div aria-level={"false"} />
<div aria-level={!"false"} />

@ -0,0 +1,47 @@
[
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-level' must be of type integer",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 21,
"character": 21
},
"pos": 5
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-level' must be of type integer",
"start": {
"line": 2,
"column": 5,
"character": 30
},
"end": {
"line": 2,
"column": 20,
"character": 45
},
"pos": 30
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-level' must be of type integer",
"start": {
"line": 5,
"column": 5,
"character": 107
},
"end": {
"line": 5,
"column": 15,
"character": 117
},
"pos": 107
}
]

@ -0,0 +1,7 @@
<div aria-valuemax="yes" />
<div aria-valuemax="no" />
<div aria-valuemax={`abc`} />
<div aria-valuemax={true} />
<div aria-valuemax />
<div aria-valuemax={'false'} />
<div aria-valuemax={!'false'} />

@ -0,0 +1,47 @@
[
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-valuemax' must be of type number",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 24,
"character": 24
},
"pos": 5
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-valuemax' must be of type number",
"start": {
"line": 2,
"column": 5,
"character": 33
},
"end": {
"line": 2,
"column": 23,
"character": 51
},
"pos": 33
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-valuemax' must be of type number",
"start": {
"line": 5,
"column": 5,
"character": 119
},
"end": {
"line": 5,
"column": 18,
"character": 132
},
"pos": 119
}
]

@ -0,0 +1,5 @@
<div aria-label />
<div aria-label={true} />
<div aria-label={false} />
<div aria-label={1234} />
<div aria-label={!true} />

@ -0,0 +1,17 @@
[
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-label' must be of type string",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 15,
"character": 15
},
"pos": 5
}
]

@ -0,0 +1,6 @@
<div aria-sort="" />
<div aria-sort="descnding" />
<div aria-sort />
<div aria-sort={true} />
<div aria-sort={"false"} />
<div aria-sort="ascending descending" />

@ -0,0 +1,62 @@
[
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-sort' must be exactly one of ascending, descending, none, other",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 17,
"character": 17
},
"pos": 5
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-sort' must be exactly one of ascending, descending, none, other",
"start": {
"line": 2,
"column": 5,
"character": 26
},
"end": {
"line": 2,
"column": 26,
"character": 47
},
"pos": 26
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-sort' must be exactly one of ascending, descending, none, other",
"start": {
"line": 3,
"column": 5,
"character": 56
},
"end": {
"line": 3,
"column": 14,
"character": 65
},
"pos": 56
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-sort' must be exactly one of ascending, descending, none, other",
"start": {
"line": 6,
"column": 5,
"character": 127
},
"end": {
"line": 6,
"column": 37,
"character": 159
},
"pos": 127
}
]

@ -0,0 +1,7 @@
<div aria-relevant="" />
<div aria-relevant="foobar" />
<div aria-relevant />
<div aria-relevant={true} />
<div aria-relevant={"false"} />
<div aria-relevant="additions removalss" />
<div aria-relevant="additions removalss " />

@ -0,0 +1,77 @@
[
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-relevant' must be a space-separated list of one or more of additions, all, removals, text",
"start": {
"line": 1,
"column": 5,
"character": 5
},
"end": {
"line": 1,
"column": 21,
"character": 21
},
"pos": 5
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-relevant' must be a space-separated list of one or more of additions, all, removals, text",
"start": {
"line": 2,
"column": 5,
"character": 30
},
"end": {
"line": 2,
"column": 27,
"character": 52
},
"pos": 30
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-relevant' must be a space-separated list of one or more of additions, all, removals, text",
"start": {
"line": 3,
"column": 5,
"character": 61
},
"end": {
"line": 3,
"column": 18,
"character": 74
},
"pos": 61
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-relevant' must be a space-separated list of one or more of additions, all, removals, text",
"start": {
"line": 6,
"column": 5,
"character": 144
},
"end": {
"line": 6,
"column": 40,
"character": 179
},
"pos": 144
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-relevant' must be a space-separated list of one or more of additions, all, removals, text",
"start": {
"line": 7,
"column": 5,
"character": 188
},
"end": {
"line": 7,
"column": 41,
"character": 224
},
"pos": 188
}
]

@ -0,0 +1,8 @@
<script>
const abc = 'abc';
</script>
<div aria-checked="yes" />
<div aria-checked="no" />
<div aria-checked={1234} />
<div aria-checked={`${abc}`} />

@ -0,0 +1,32 @@
[
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-checked' must be exactly one of true, false, or mixed",
"start": {
"line": 5,
"column": 5,
"character": 48
},
"end": {
"line": 5,
"column": 23,
"character": 66
},
"pos": 48
},
{
"code": "a11y-incorrect-aria-attribute-type",
"message": "A11y: The value of 'aria-checked' must be exactly one of true, false, or mixed",
"start": {
"line": 6,
"column": 5,
"character": 75
},
"end": {
"line": 6,
"column": 22,
"character": 92
},
"pos": 75
}
]
Loading…
Cancel
Save