diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts
index 07f17390182e..e7115dad0c02 100644
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -17,6 +17,9 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Component from '../Component';
+import { ARIARoleDefintionKey } from 'aria-query';
+import { noninteractive_roles } from '../utils/aria_roles';
+import { interactive_elements } from '../utils/elements';
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)$/;
@@ -596,6 +599,18 @@ export default class Element extends Node {
});
}
}
+
+ if (interactive_elements.has(this.name)) {
+ if (attribute_map.has('role')) {
+ const roleValue = this.attributes.find(a => a.name === 'role').get_static_value().toString() as ARIARoleDefintionKey;
+ if (noninteractive_roles.has(roleValue)) {
+ component.warn(this, {
+ code: 'a11y-no-interactive-element-to-noninteractive-role',
+ message: `A11y: <${this.name}> cannot have role ${roleValue}`
+ });
+ }
+ }
+ }
}
validate_bindings_foreign() {
diff --git a/src/compiler/compile/utils/aria_roles.ts b/src/compiler/compile/utils/aria_roles.ts
new file mode 100644
index 000000000000..304e76cc9738
--- /dev/null
+++ b/src/compiler/compile/utils/aria_roles.ts
@@ -0,0 +1,9 @@
+import { roles as rolesMap } from 'aria-query';
+
+const roles = [...rolesMap.keys()];
+
+const noninteractive_roles = new Set(roles
+ .filter((name) => !rolesMap.get(name).abstract)
+ .filter((name) => !rolesMap.get(name).superClass.some((c) => c.includes('widget'))));
+
+export { noninteractive_roles };
diff --git a/src/compiler/compile/utils/elements.ts b/src/compiler/compile/utils/elements.ts
new file mode 100644
index 000000000000..eee1ec271ad7
--- /dev/null
+++ b/src/compiler/compile/utils/elements.ts
@@ -0,0 +1,5 @@
+const interactive_elements = new Set([
+ 'a', 'button', 'input', 'select', 'textarea'
+]);
+
+export { interactive_elements };
diff --git a/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/input.svelte b/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/input.svelte
new file mode 100644
index 000000000000..ad12bcfd069d
--- /dev/null
+++ b/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/input.svelte
@@ -0,0 +1,54 @@
+
+link
+link
+link
+link
+link
+link
+link
+link
+link
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/warnings.json b/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/warnings.json
new file mode 100644
index 000000000000..686a93940392
--- /dev/null
+++ b/test/validator/samples/a11y-no-interactive-element-to-noninteractive-role/warnings.json
@@ -0,0 +1,602 @@
+[
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 49,
+ "column": 38,
+ "line": 2
+ },
+ "message": "A11y: cannot have role article",
+ "pos": 11,
+ "start": {
+ "character": 11,
+ "column": 0,
+ "line": 2
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 87,
+ "column": 37,
+ "line": 3
+ },
+ "message": "A11y: cannot have role banner",
+ "pos": 50,
+ "start": {
+ "character": 50,
+ "column": 0,
+ "line": 3
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 132,
+ "column": 44,
+ "line": 4
+ },
+ "message": "A11y: cannot have role complementary",
+ "pos": 88,
+ "start": {
+ "character": 88,
+ "column": 0,
+ "line": 4
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 167,
+ "column": 34,
+ "line": 5
+ },
+ "message": "A11y: cannot have role img",
+ "pos": 133,
+ "start": {
+ "character": 133,
+ "column": 0,
+ "line": 5
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 207,
+ "column": 39,
+ "line": 6
+ },
+ "message": "A11y: cannot have role listitem",
+ "pos": 168,
+ "start": {
+ "character": 168,
+ "column": 0,
+ "line": 6
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 243,
+ "column": 35,
+ "line": 7
+ },
+ "message": "A11y: cannot have role main",
+ "pos": 208,
+ "start": {
+ "character": 208,
+ "column": 0,
+ "line": 7
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 281,
+ "column": 37,
+ "line": 8
+ },
+ "message": "A11y: cannot have role region",
+ "pos": 244,
+ "start": {
+ "character": 244,
+ "column": 0,
+ "line": 8
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 320,
+ "column": 38,
+ "line": 9
+ },
+ "message": "A11y: cannot have role tooltip",
+ "pos": 282,
+ "start": {
+ "character": 282,
+ "column": 0,
+ "line": 9
+ }
+ },
+ {
+ "code": "a11y-no-interactive-element-to-noninteractive-role",
+ "end": {
+ "character": 414,
+ "column": 38,
+ "line": 13
+ },
+ "message": "A11y: