diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts
index a851bc24c2c8..e9fc80cabef4 100644
--- a/src/compiler/compile/compiler_warnings.ts
+++ b/src/compiler/compile/compiler_warnings.ts
@@ -115,6 +115,10 @@ export default {
code: 'a11y-no-redundant-roles',
message: `A11y: Redundant role '${role}'`
}),
+ a11y_no_static_element_interactions: (element: string, handlers: string[]) => ({
+ code: 'a11y-no-static-element-interactions',
+ message: `A11y: <${element}> with ${handlers.join(', ')} ${handlers.length === 1 ? 'handler' : 'handlers'} must have an ARIA role`
+ }),
a11y_no_interactive_element_to_noninteractive_role: (role: string | boolean, element: string) => ({
code: 'a11y-no-interactive-element-to-noninteractive-role',
message: `A11y: <${element}> cannot have role '${role}'`
diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts
index 2410904d6301..3e6d886a5d35 100644
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -738,8 +738,10 @@ export default class Element extends Node {
}
}
+ const role = attribute_map.get('role')?.get_static_value() as ARIARoleDefinitionKey;
+
// no-noninteractive-tabindex
- if (!this.is_dynamic_element && !is_interactive_element(this.name, attribute_map) && !is_interactive_roles(attribute_map.get('role')?.get_static_value() as ARIARoleDefinitionKey)) {
+ if (!this.is_dynamic_element && !is_interactive_element(this.name, attribute_map) && !is_interactive_roles(role)) {
const tab_index = attribute_map.get('tabindex');
if (tab_index && (!tab_index.is_static || Number(tab_index.get_static_value()) >= 0)) {
component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex);
@@ -747,8 +749,7 @@ export default class Element extends Node {
}
// role-supports-aria-props
- const role = attribute_map.get('role');
- const role_value = (role ? role.get_static_value() : get_implicit_role(this.name, attribute_map)) as ARIARoleDefinitionKey;
+ const role_value = (role ?? get_implicit_role(this.name, attribute_map)) as ARIARoleDefinitionKey;
if (typeof role_value === 'string' && roles.has(role_value)) {
const { props } = roles.get(role_value);
const invalid_aria_props = new Set(aria.keys().filter(attribute => !(attribute in props)));
@@ -762,6 +763,30 @@ export default class Element extends Node {
}
});
}
+
+ const has_dynamic_role = attribute_map.get('role') && !attribute_map.get('role').is_static;
+
+ // no-static-element-interactions
+ if (
+ !has_dynamic_role &&
+ !is_hidden_from_screen_reader(this.name, attribute_map) &&
+ !is_presentation_role(role) &&
+ !is_interactive_element(this.name, attribute_map) &&
+ !is_interactive_roles(role) &&
+ !is_non_interactive_element(this.name, attribute_map) &&
+ !is_non_interactive_roles(role) &&
+ !is_abstract_role(role)
+ ) {
+ const interactive_handlers = handlers
+ .map((handler) => handler.name)
+ .filter((handlerName) => a11y_interactive_handlers.has(handlerName));
+ if (interactive_handlers.length > 0) {
+ component.warn(
+ this,
+ compiler_warnings.a11y_no_static_element_interactions(this.name, interactive_handlers)
+ );
+ }
+ }
}
validate_special_cases() {
diff --git a/src/compiler/compile/utils/a11y.ts b/src/compiler/compile/utils/a11y.ts
index 4409f802623d..bc23f5c81866 100644
--- a/src/compiler/compile/utils/a11y.ts
+++ b/src/compiler/compile/utils/a11y.ts
@@ -19,7 +19,8 @@ const non_interactive_roles = new Set(
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
// focusable tabpanel elements are recommended if any panels in a set contain content where the first element in the panel is not focusable.
- !['toolbar', 'tabpanel'].includes(name) &&
+ // 'generic' is meant to have no semantic meaning.
+ !['toolbar', 'tabpanel', 'generic'].includes(name) &&
!role.superClass.some((classes) => classes.includes('widget'))
);
})
@@ -31,7 +32,11 @@ const non_interactive_roles = new Set(
);
const interactive_roles = new Set(
- non_abstract_roles.filter((name) => !non_interactive_roles.has(name))
+ non_abstract_roles.filter((name) =>
+ !non_interactive_roles.has(name) &&
+ // 'generic' is meant to have no semantic meaning.
+ name !== 'generic'
+ )
);
export function is_non_interactive_roles(role: ARIARoleDefinitionKey) {
diff --git a/test/validator/samples/a11y-click-events-have-key-events/input.svelte b/test/validator/samples/a11y-click-events-have-key-events/input.svelte
index 8737f04ec586..c6ac9ac86616 100644
--- a/test/validator/samples/a11y-click-events-have-key-events/input.svelte
+++ b/test/validator/samples/a11y-click-events-have-key-events/input.svelte
@@ -9,12 +9,16 @@
+
+
+
+
@@ -28,24 +32,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/validator/samples/a11y-click-events-have-key-events/warnings.json b/test/validator/samples/a11y-click-events-have-key-events/warnings.json
index 904c733a9937..42eaed00252c 100644
--- a/test/validator/samples/a11y-click-events-have-key-events/warnings.json
+++ b/test/validator/samples/a11y-click-events-have-key-events/warnings.json
@@ -3,11 +3,11 @@
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.",
"start": {
- "line": 12,
+ "line": 13,
"column": 0
},
"end": {
- "line": 12,
+ "line": 13,
"column": 23
}
},
@@ -15,11 +15,11 @@
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.",
"start": {
- "line": 13,
+ "line": 15,
"column": 0
},
"end": {
- "line": 13,
+ "line": 15,
"column": 43
}
},
@@ -27,11 +27,11 @@
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.",
"start": {
- "line": 15,
+ "line": 18,
"column": 0
},
"end": {
- "line": 15,
+ "line": 18,
"column": 27
}
},
@@ -39,11 +39,11 @@
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.",
"start": {
- "line": 16,
+ "line": 19,
"column": 0
},
"end": {
- "line": 16,
+ "line": 19,
"column": 24
}
},
@@ -51,11 +51,11 @@
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.",
"start": {
- "line": 17,
+ "line": 20,
"column": 0
},
"end": {
- "line": 17,
+ "line": 20,
"column": 27
}
},
@@ -63,11 +63,11 @@
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.",
"start": {
- "line": 18,
+ "line": 22,
"column": 0
},
"end": {
- "line": 18,
+ "line": 22,
"column": 26
}
},
@@ -75,11 +75,11 @@
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with an on:click event must be accompanied by an on:keydown, on:keyup, or on:keypress event.",
"start": {
- "line": 19,
+ "line": 23,
"column": 0
},
"end": {
- "line": 19,
+ "line": 23,
"column": 26
}
}
diff --git a/test/validator/samples/a11y-mouse-events-have-key-events/input.svelte b/test/validator/samples/a11y-mouse-events-have-key-events/input.svelte
index ae67bcd307c7..493f637c7509 100644
--- a/test/validator/samples/a11y-mouse-events-have-key-events/input.svelte
+++ b/test/validator/samples/a11y-mouse-events-have-key-events/input.svelte
@@ -7,9 +7,15 @@
};
+
void 0} />
+
void 0} on:focus={() => void 0} />
+
void 0} {...otherProps} />
+
void 0} />
+
void 0} on:blur={() => void 0} />
+
void 0} {...otherProps} />
diff --git a/test/validator/samples/a11y-mouse-events-have-key-events/warnings.json b/test/validator/samples/a11y-mouse-events-have-key-events/warnings.json
index e67a2a718f0f..7aae042a7943 100644
--- a/test/validator/samples/a11y-mouse-events-have-key-events/warnings.json
+++ b/test/validator/samples/a11y-mouse-events-have-key-events/warnings.json
@@ -3,48 +3,48 @@
"code": "a11y-mouse-events-have-key-events",
"end": {
"column": 35,
- "line": 10
+ "line": 11
},
"message": "A11y: on:mouseover must be accompanied by on:focus",
"start": {
"column": 0,
- "line": 10
+ "line": 11
}
},
{
"code": "a11y-mouse-events-have-key-events",
"end": {
"column": 51,
- "line": 12
+ "line": 15
},
"message": "A11y: on:mouseover must be accompanied by on:focus",
"start": {
"column": 0,
- "line": 12
+ "line": 15
}
},
{
"code": "a11y-mouse-events-have-key-events",
"end": {
"column": 34,
- "line": 13
+ "line": 17
},
"message": "A11y: on:mouseout must be accompanied by on:blur",
"start": {
"column": 0,
- "line": 13
+ "line": 17
}
},
{
"code": "a11y-mouse-events-have-key-events",
"end": {
"column": 50,
- "line": 15
+ "line": 21
},
"message": "A11y: on:mouseout must be accompanied by on:blur",
"start": {
"column": 0,
- "line": 15
+ "line": 21
}
}
]
diff --git a/test/validator/samples/a11y-no-static-element-interactions/input.svelte b/test/validator/samples/a11y-no-static-element-interactions/input.svelte
new file mode 100644
index 000000000000..06a389d025d3
--- /dev/null
+++ b/test/validator/samples/a11y-no-static-element-interactions/input.svelte
@@ -0,0 +1,18 @@
+
+
+
+