diff --git a/site/content/docs/06-accessibility-warnings.md b/site/content/docs/06-accessibility-warnings.md
index 7aa3873cce72..ee52aab405ea 100644
--- a/site/content/docs/06-accessibility-warnings.md
+++ b/site/content/docs/06-accessibility-warnings.md
@@ -308,6 +308,20 @@ Elements with ARIA roles must have all required attributes for that role.
---
+### `a11y-role-supports-aria-props`
+
+Elements with explicit or implicit roles defined contain only `aria-*` properties supported by that role.
+
+```sv
+
+
+
+
+
+```
+
+---
+
### `a11y-structure`
Enforce that certain DOM elements have the correct structure.
diff --git a/src/compiler/compile/compiler_warnings.ts b/src/compiler/compile/compiler_warnings.ts
index a10fe6155ca7..817aabea02f0 100644
--- a/src/compiler/compile/compiler_warnings.ts
+++ b/src/compiler/compile/compiler_warnings.ts
@@ -123,6 +123,17 @@ export default {
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_role_supports_aria_props: (attribute: string, role: string, is_implicit: boolean, name: string) => {
+ let message = `The attribute '${attribute}' is not supported by the role '${role}'.`;
+ if (is_implicit) {
+ message += ` This role is implicit on the element <${name}>.`;
+ }
+
+ return {
+ code: 'a11y-role-supports-aria-props',
+ message: `A11y: ${message}`
+ };
+ },
a11y_accesskey: {
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts
index 1ac75faffdde..ed1b9d64bff9 100644
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -82,11 +82,15 @@ const a11y_nested_implicit_semantics = new Map([
const a11y_implicit_semantics = new Map([
['a', 'link'],
+ ['area', 'link'],
+ ['article', 'article'],
['aside', 'complementary'],
['body', 'document'],
+ ['button', 'button'],
['datalist', 'listbox'],
['dd', 'definition'],
['dfn', 'term'],
+ ['dialog', 'dialog'],
['details', 'group'],
['dt', 'term'],
['fieldset', 'group'],
@@ -98,10 +102,14 @@ const a11y_implicit_semantics = new Map([
['h5', 'heading'],
['h6', 'heading'],
['hr', 'separator'],
+ ['img', 'img'],
['li', 'listitem'],
+ ['link', 'link'],
['menu', 'list'],
+ ['meter', 'progressbar'],
['nav', 'navigation'],
['ol', 'list'],
+ ['option', 'option'],
['optgroup', 'group'],
['output', 'status'],
['progress', 'progressbar'],
@@ -115,6 +123,61 @@ const a11y_implicit_semantics = new Map([
['ul', 'list']
]);
+const menuitem_type_to_implicit_role = new Map([
+ ['command', 'menuitem'],
+ ['checkbox', 'menuitemcheckbox'],
+ ['radio', 'menuitemradio']
+]);
+
+const input_type_to_implicit_role = new Map([
+ ['button', 'button'],
+ ['image', 'button'],
+ ['reset', 'button'],
+ ['submit', 'button'],
+ ['checkbox', 'checkbox'],
+ ['radio', 'radio'],
+ ['range', 'slider'],
+ ['number', 'spinbutton'],
+ ['email', 'textbox'],
+ ['search', 'searchbox'],
+ ['tel', 'textbox'],
+ ['text', 'textbox'],
+ ['url', 'textbox']
+]);
+
+const combobox_if_list = new Set(['email', 'search', 'tel', 'text', 'url']);
+
+function input_implicit_role(attribute_map: Map) {
+ const type_attribute = attribute_map.get('type');
+ if (!type_attribute || !type_attribute.is_static) return;
+ const type = type_attribute.get_static_value() as string;
+
+ const list_attribute_exists = attribute_map.has('list');
+
+ if (list_attribute_exists && combobox_if_list.has(type)) {
+ return 'combobox';
+ }
+
+ return input_type_to_implicit_role.get(type);
+}
+
+function menuitem_implicit_role(attribute_map: Map) {
+ const type_attribute = attribute_map.get('type');
+ if (!type_attribute || !type_attribute.is_static) return;
+ const type = type_attribute.get_static_value() as string;
+ return menuitem_type_to_implicit_role.get(type);
+}
+
+function get_implicit_role(name: string, attribute_map: Map) : (string | undefined) {
+ if (name === 'menuitem') {
+ return menuitem_implicit_role(attribute_map);
+ } else if (name === 'input') {
+ return input_implicit_role(attribute_map);
+ } else {
+ return a11y_implicit_semantics.get(name);
+ }
+}
+
const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set([
@@ -488,7 +551,7 @@ export default class Element extends Node {
// aria-activedescendant-has-tabindex
if (name === 'aria-activedescendant' && !is_interactive_element(this.name, attribute_map) && !attribute_map.has('tabindex')) {
- component.warn(attribute, compiler_warnings.a11y_aria_activedescendant_has_tabindex);
+ component.warn(attribute, compiler_warnings.a11y_aria_activedescendant_has_tabindex);
}
}
@@ -511,7 +574,7 @@ export default class Element extends Node {
}
// no-redundant-roles
- const has_redundant_role = current_role === a11y_implicit_semantics.get(this.name);
+ const has_redundant_role = current_role === get_implicit_role(this.name, attribute_map);
if (this.name === current_role || has_redundant_role) {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(current_role));
@@ -605,6 +668,23 @@ export default class Element extends Node {
component.warn(this, compiler_warnings.a11y_no_noninteractive_tabindex);
}
}
+
+ // 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 ARIARoleDefintionKey;
+ 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)));
+ const is_implicit = role_value && role === undefined;
+
+ attributes
+ .filter(prop => prop.type !== 'Spread')
+ .forEach(prop => {
+ if (invalid_aria_props.has(prop.name as ARIAProperty)) {
+ component.warn(prop, compiler_warnings.a11y_role_supports_aria_props(prop.name, role_value, is_implicit, this.name));
+ }
+ });
+ }
}
validate_special_cases() {
diff --git a/test/validator/samples/a11y-role-has-required-aria-props/input.svelte b/test/validator/samples/a11y-role-has-required-aria-props/input.svelte
index 113ed6531630..8f72ff04bffc 100644
--- a/test/validator/samples/a11y-role-has-required-aria-props/input.svelte
+++ b/test/validator/samples/a11y-role-has-required-aria-props/input.svelte
@@ -8,4 +8,3 @@
-
\ No newline at end of file
diff --git a/test/validator/samples/a11y-role-supports-aria-props/input.svelte b/test/validator/samples/a11y-role-supports-aria-props/input.svelte
new file mode 100644
index 000000000000..353069dc06ce
--- /dev/null
+++ b/test/validator/samples/a11y-role-supports-aria-props/input.svelte
@@ -0,0 +1,371 @@
+
+Link
+
+
+
+
+
+
+
+
+
+
+
+
+
+