Skip to content

Commit 1d19aeb

Browse files
melnarytanhauhau
andauthored
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 <[email protected]>
1 parent c01dc62 commit 1d19aeb

File tree

8 files changed

+867
-633
lines changed

8 files changed

+867
-633
lines changed

package-lock.json

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,14 @@
124124
"@rollup/plugin-typescript": "^2.0.1",
125125
"@rollup/plugin-virtual": "^2.0.0",
126126
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.8.0",
127+
"@types/aria-query": "^5.0.0",
127128
"@types/mocha": "^7.0.0",
128129
"@types/node": "^8.10.53",
129130
"@typescript-eslint/eslint-plugin": "^5.22.0",
130131
"@typescript-eslint/parser": "^5.22.0",
131132
"acorn": "^8.4.1",
132133
"agadoo": "^1.1.0",
134+
"aria-query": "^5.0.0",
133135
"code-red": "^0.2.5",
134136
"css-tree": "^1.1.2",
135137
"eslint": "^8.0.0",

site/content/docs/05-accessibility-warnings.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ The following elements are visually distracting: `<marquee>` and `<blink>`.
5454

5555
---
5656

57+
### `role-has-required-aria-props`
58+
59+
Elements with ARIA roles must have all required attributes for that role.
60+
61+
```sv
62+
<!-- A11y: A11y: Elements with the ARIA role "checkbox" must have the following attributes defined: "aria-checked" -->
63+
<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>
64+
```
65+
66+
---
67+
5768
### `a11y-hidden`
5869

5970
Certain DOM elements are useful for screen reader navigation and should not be hidden.

src/compiler/compile/compiler_warnings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export default {
8080
code: 'a11y-no-redundant-roles',
8181
message: `A11y: Redundant role '${role}'`
8282
}),
83+
a11y_role_has_required_aria_props: (role: string, props: string[]) => ({
84+
code: 'a11y-role-has-required-aria-props',
85+
message: `A11y: Elements with the ARIA role "${role}" must have the following attributes defined: ${props.map(name => `"${name}"`).join(', ')}`
86+
}),
8387
a11y_accesskey: {
8488
code: 'a11y-accesskey',
8589
message: 'A11y: Avoid using accesskey'

src/compiler/compile/nodes/Element.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { string_literal } from '../utils/stringify';
2323
import { Literal } from 'estree';
2424
import compiler_warnings from '../compiler_warnings';
2525
import compiler_errors from '../compiler_errors';
26+
import { ARIARoleDefintionKey, roles } from 'aria-query';
2627

2728
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)$/;
2829

@@ -407,9 +408,9 @@ export default class Element extends Node {
407408
}
408409

409410
validate_attributes_a11y() {
410-
const { component } = this;
411+
const { component, attributes } = this;
411412

412-
this.attributes.forEach(attribute => {
413+
attributes.forEach(attribute => {
413414
if (attribute.is_spread) return;
414415

415416
const name = attribute.name.toLowerCase();
@@ -462,6 +463,17 @@ export default class Element extends Node {
462463
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
463464
}
464465
}
466+
467+
// role-has-required-aria-props
468+
const role = roles.get(value as ARIARoleDefintionKey);
469+
if (role) {
470+
const required_role_props = Object.keys(role.requiredProps);
471+
const has_missing_props = required_role_props.some(prop => !attributes.find(a => a.name === prop));
472+
473+
if (has_missing_props) {
474+
component.warn(attribute, compiler_warnings.a11y_role_has_required_aria_props(value as string, required_role_props));
475+
}
476+
}
465477
}
466478

467479
// no-access-key

0 commit comments

Comments
 (0)