Repository: sveltejs/svelte (JavaScript)
Date: 2022-09-05 15:46:10+00:00
Task type: Bug Fix

Issue:
A11y warning `role="switch" ... must have "aria-checked"` is a false positive for native `input` elements
### Describe the bug

Svelte v3.50.0 added additional accessibility checks in #5852.

`role-has-required-aria-props` now emits the following warning:

```html
A11y: Elements with the ARIA role "switch" must have the following attributes defined: "aria-checked"

<input role="switch" type="checkbox" checked />
```

Per MDN, [ARIA attributes are not required](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-checked) if using the native `input` element with `type="checkbox"` or `type="radio"` because it has its own semantics (i.e., `checked`).

As a result, the a11y warning for the above use case is a false positive.

I encountered this in https://github.com/carbon-design-system/carbon-components-svelte/pull/1480. The workaround is to [ignore the warning](https://github.com/carbon-design-system/carbon-components-svelte/pull/1480/commits/0f694254ff72bf48cdb2b70040c6cc3363e48fe4).

### Reproduction

https://svelte.dev/repl/ea3cff693d6f4b8caa80e651cbaf1eae?version=3.50.0

### Logs

_No response_

### System Info

```shell
System:
  OS: macOS 12.1
  CPU: (8) x64 Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  Memory: 20.60 MB / 16.00 GB
  Shell: 5.8 - /bin/zsh
Binaries:
  Node: 16.14.2 - ~/.nvm/versions/node/v16.14.2/bin/node
  Yarn: 1.22.19 - /usr/local/bin/yarn
  npm: 8.5.0 - ~/.nvm/versions/node/v16.14.2/bin/npm
Browsers:
  Chrome: 105.0.5195.52
  Firefox: 103.0
  Safari: 15.2
npmPackages:
  rollup: ^1.27.14 => 1.27.14
```


### Severity

annoyance


Issue discussion:

Solution patch:
diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts
--- a/src/compiler/compile/nodes/Element.ts
+++ b/src/compiler/compile/nodes/Element.ts
@@ -24,7 +24,7 @@ import { Literal } from 'estree';
 import compiler_warnings from '../compiler_warnings';
 import compiler_errors from '../compiler_errors';
 import { ARIARoleDefintionKey, roles, aria, ARIAPropertyDefinition, ARIAProperty } from 'aria-query';
-import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader } from '../utils/a11y';
+import { is_interactive_element, is_non_interactive_roles, is_presentation_role, is_interactive_roles, is_hidden_from_screen_reader, is_semantic_role_element } from '../utils/a11y';
 
 const aria_attributes = 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(' ');
 const aria_attribute_set = new Set(aria_attributes);
@@ -514,13 +514,15 @@ export default class Element extends Node {
 				}
 
 				// role-has-required-aria-props
-				const role = roles.get(value);
-				if (role) {
-					const required_role_props = Object.keys(role.requiredProps);
-					const has_missing_props = required_role_props.some(prop => !attributes.find(a => a.name === prop));
-
-					if (has_missing_props) {
-						component.warn(attribute, compiler_warnings.a11y_role_has_required_aria_props(value as string, required_role_props));
+				if (!is_semantic_role_element(value, this.name, attribute_map)) {
+					const role = roles.get(value);
+					if (role) {
+						const required_role_props = Object.keys(role.requiredProps);
+						const has_missing_props = required_role_props.some(prop => !attributes.find(a => a.name === prop));
+
+						if (has_missing_props) {
+							component.warn(attribute, compiler_warnings.a11y_role_has_required_aria_props(value as string, required_role_props));
+						}
 					}
 				}
 
diff --git a/src/compiler/compile/utils/a11y.ts b/src/compiler/compile/utils/a11y.ts
--- a/src/compiler/compile/utils/a11y.ts
+++ b/src/compiler/compile/utils/a11y.ts
@@ -4,7 +4,7 @@ import {
 	elementRoles,
 	ARIARoleRelationConcept
 } from 'aria-query';
-import { AXObjects, elementAXObjects } from 'axobject-query';
+import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query';
 import Attribute from '../nodes/Attribute';
 
 const roles = [...roles_map.keys()];
@@ -119,8 +119,8 @@ function match_schema(
 			schema_attribute.value &&
 			schema_attribute.value !== attribute.get_static_value()
 		) {
-return false;
-}
+			return false;
+		}
 		return true;
 	});
 }
@@ -155,3 +155,23 @@ export function is_interactive_element(
 
 	return false;
 }
+
+export function is_semantic_role_element(role: ARIARoleDefintionKey, tag_name: string, attribute_map: Map<string, Attribute>) {
+	for (const [schema, ax_object] of elementAXObjects.entries()) {
+		if (schema.name === tag_name && (!schema.attributes || schema.attributes.every(
+			(attr) => attribute_map.has(attr.name) && attribute_map.get(attr.name).get_static_value() === attr.value
+		))) {
+			for (const name of ax_object) {
+				const roles = AXObjectRoles.get(name);
+				if (roles) {
+					for (const { name } of roles) {
+						if (name === role) {
+							return true;
+						}
+					}
+				}
+			}
+		}
+	}
+	return false;
+}


Test patch:
diff --git a/test/validator/samples/a11y-no-redundant-roles/warnings.json b/test/validator/samples/a11y-no-redundant-roles/warnings.json
--- a/test/validator/samples/a11y-no-redundant-roles/warnings.json
+++ b/test/validator/samples/a11y-no-redundant-roles/warnings.json
@@ -224,21 +224,6 @@
 			"line": 15
 		}
 	},
-	{
-		"code": "a11y-role-has-required-aria-props",
-		"end": {
-			"character": 383,
-			"column": 18,
-			"line": 15
-		},
-		"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
-		"pos": 369,
-		"start": {
-			"character": 369,
-			"column": 4,
-			"line": 15
-		}
-	},
 	{
 		"code": "a11y-no-redundant-roles",
 		"end": {
@@ -254,21 +239,6 @@
 			"line": 16
 		}
 	},
-	{
-		"code": "a11y-role-has-required-aria-props",
-		"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
-		"pos": 401,
-		"start": {
-			"character": 401,
-			"column": 4,
-			"line": 16
-		},
-		"end": {
-			"character": 415,
-			"column": 18,
-			"line": 16
-		}
-	},
 	{
 		"code": "a11y-no-redundant-roles",
 		"end": {
@@ -284,21 +254,6 @@
 			"line": 17
 		}
 	},
-	{
-		"code": "a11y-role-has-required-aria-props",
-		"end": {
-			"character": 447,
-			"column": 18,
-			"line": 17
-		},
-		"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
-		"pos": 433,
-		"start": {
-			"character": 433,
-			"column": 4,
-			"line": 17
-		}
-	},
 	{
 		"code": "a11y-no-redundant-roles",
 		"end": {
@@ -314,21 +269,6 @@
 			"line": 18
 		}
 	},
-	{
-		"code": "a11y-role-has-required-aria-props",
-		"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
-		"pos": 465,
-		"start": {
-			"character": 465,
-			"column": 4,
-			"line": 18
-		},
-		"end": {
-			"character": 479,
-			"column": 18,
-			"line": 18
-		}
-	},
 	{
 		"code": "a11y-no-redundant-roles",
 		"end": {
@@ -344,21 +284,6 @@
 			"line": 19
 		}
 	},
-	{
-		"code": "a11y-role-has-required-aria-props",
-		"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
-		"pos": 497,
-		"start": {
-			"character": 497,
-			"column": 4,
-			"line": 19
-		},
-		"end": {
-			"character": 511,
-			"column": 18,
-			"line": 19
-		}
-	},
 	{
 		"code": "a11y-no-redundant-roles",
 		"end": {
@@ -374,21 +299,6 @@
 			"line": 20
 		}
 	},
-	{
-		"code": "a11y-role-has-required-aria-props",
-		"message": "A11y: Elements with the ARIA role \"heading\" must have the following attributes defined: \"aria-level\"",
-		"pos": 529,
-		"start": {
-			"character": 529,
-			"column": 4,
-			"line": 20
-		},
-		"end": {
-			"character": 543,
-			"column": 18,
-			"line": 20
-		}
-	},
 	{
 		"code": "a11y-no-redundant-roles",
 		"end": {
@@ -524,21 +434,6 @@
 			"line": 29
 		}
 	},
-	{
-		"code": "a11y-role-has-required-aria-props",
-		"message": "A11y: Elements with the ARIA role \"option\" must have the following attributes defined: \"aria-selected\"",
-		"pos": 751,
-		"start": {
-			"character": 751,
-			"column": 8,
-			"line": 29
-		},
-		"end": {
-			"character": 764,
-			"column": 21,
-			"line": 29
-		}
-	},
 	{
 		"code": "a11y-no-redundant-roles",
 		"end": {
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
--- a/test/validator/samples/a11y-role-has-required-aria-props/input.svelte
+++ b/test/validator/samples/a11y-role-has-required-aria-props/input.svelte
@@ -7,3 +7,5 @@
 <span role="checkbox" aria-checked="false"></span>
 <div role="meter" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
 <div role="scrollbar" aria-controls="panel" aria-valuenow="50"></div>
+<input role="switch" type="checkbox" />
+<input role="radio" type="radio" />
\ No newline at end of file