rule: role-has-required-aria-props
MODIFY
packages/@romejs/compiler/lint/rules/ariaHelpers.ts
packages/@romejs/compiler/lint/rules/ariaHelpers.ts
@@ -374,3 +374,96 @@ export const ariaPropsMap: MapOfARIAPropertyDefinitions = new Map([
},
],
]);
+
+export type ARIARoleDefinition = {
+ props: Array<ARIAProperty>;
+ requiredProps: Array<ARIAProperty>;
+};
+
+export type MapOfAriaRoles = Map<string, ARIARoleDefinition>;
+
+export const roles: MapOfAriaRoles = new Map([
+ [
+ "checkbox",
+ {
+ props: ["aria-checked", "aria-readonly"],
+ requiredProps: ["aria-checked"],
+ },
+ ],
+ [
+ "radio",
+ {
+ props: ["aria-checked", "aria-readonly"],
+ requiredProps: ["aria-checked"],
+ },
+ ],
+ [
+ "switch",
+ {
+ props: ["aria-checked"],
+ requiredProps: ["aria-checked"],
+ },
+ ],
+ [
+ "option",
+ {
+ props: ["aria-selected"],
+ requiredProps: ["aria-selected"],
+ },
+ ],
+ [
+ "combobox",
+ {
+ props: ["aria-controls", "aria-expanded"],
+ requiredProps: ["aria-controls", "aria-expanded"],
+ },
+ ],
+ [
+ "heading",
+ {
+ props: ["aria-level"],
+ requiredProps: ["aria-level"],
+ },
+ ],
+ [
+ "spinbutton",
+ {
+ props: ["aria-valuemax", "aria-valuemin", "aria-valuenow"],
+ requiredProps: ["aria-valuemax", "aria-valuemin", "aria-valuenow"],
+ },
+ ],
+ [
+ "slider",
+ {
+ props: ["aria-valuemax", "aria-valuemin", "aria-valuenow"],
+ requiredProps: ["aria-valuemax", "aria-valuemin", "aria-valuenow"],
+ },
+ ],
+ [
+ "separator",
+ {
+ props: ["aria-valuemax", "aria-valuemin", "aria-valuenow"],
+ requiredProps: ["aria-valuemax", "aria-valuemin", "aria-valuenow"],
+ },
+ ],
+
+ [
+ "scrollbar",
+ {
+ props: [
+ "aria-valuemax",
+ "aria-valuemin",
+ "aria-valuenow",
+ "aria-orientation",
+ "aria-controls",
+ ],
+ requiredProps: [
+ "aria-valuemax",
+ "aria-valuemin",
+ "aria-valuenow",
+ "aria-orientation",
+ "aria-controls",
+ ],
+ },
+ ],
+]);

MODIFY
packages/@romejs/compiler/lint/rules/index.ts
packages/@romejs/compiler/lint/rules/index.ts
@@ -72,6 +72,7 @@ import noAutofocus from "./jsx-a11y/noAutofocus";
import noDistractingElements from "./jsx-a11y/noDistractingElements";
import noOnChange from "./jsx-a11y/noOnChange";
import noTargetBlank from "./jsx-a11y/noTargetBlank";
+import roleHasRequiredAriaProps from "./jsx-a11y/roleHasRequiredAriaProps";
import scope from "./jsx-a11y/scope";
import tabindexNoPositive from "./jsx-a11y/tabindexNoPositive";
import buttonHasType from "./react/buttonHasType";
@@ -161,6 +162,7 @@ export const lintTransforms = [
noDistractingElements,
noOnChange,
noTargetBlank,
+ roleHasRequiredAriaProps,
scope,
tabindexNoPositive,
buttonHasType,

ADD
packages/@romejs/compiler/lint/rules/jsx-a11y/roleHasRequiredAriaProps.test.ts
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ "jsx a11y role has required aria props",
+ async (t) => {
+ await testLintMultiple(
+ t,
+ [
+ // INVALID
+ "<span role='checkbox'></span>",
+ "<span role='switch'></span>",
+ "<span role='spinbutton'></span>",
+ "<span role='spinbutton' aria-valuemax='100'></span>",
+ "<span role='spinbutton' aria-valuemin='0'></span>",
+ "<span role='spinbutton' aria-valuemin='0' aria-valuemax='100'></span>",
+ "<span role='slider' aria-valuemin='0'></span>",
+ "<span role='slider' aria-valuemax='100'></span>",
+ "<span role='slider' aria-valuemin='0' aria-valuemax='100'></span>",
+ "<span role='separator' aria-valuemin='0'></span>",
+ "<span role='separator' aria-valuemax='100'></span>",
+ "<span role='separator' aria-valuemin='0' aria-valuemax='100'></span>",
+ "<span role='scrollbar' aria-valuemin='0'></span>",
+ "<span role='scrollbar' aria-valuemax='100'></span>",
+ "<span role='scrollbar' aria-valuemin='0' aria-valuemax='100'></span>",
+ "<span role='scrollbar' aria-valuemin='0' aria-valuemax='100' aria-orientation='horizontal'></span>",
+ "<span role='radio'></span>",
+ "<span role='option'></span>",
+ "<span role='heading'></span>",
+ "<span role='combobox'></span>",
+ "<span role='combobox' aria-expanded='true'></span>",
+ "<span role='combobox' aria-controls='true'></span>",
+ // VALID
+ "<span role='checkbox' aria-checked='true'></span>",
+ "<span role='switch' aria-checked='true'></span>",
+ "<span role='spinbutton' aria-valuemax='100' aria-valuemin='0' aria-valuenow='50'></span>",
+ "<span role='slider' aria-valuemax='100' aria-valuemin='0' aria-valuenow='50'></span>",
+ "<span role='separator' aria-valuemax='100' aria-valuemin='0' aria-valuenow='50'></span>",
+ "<span role='scrollbar' aria-valuemax='100' aria-valuemin='0' aria-valuenow='50' aria-orientation='horizontal' aria-controls='123'></span>",
+ "<span role='radio' aria-checked='true'></span>",
+ "<span role='option' aria-selected='true'></span>",
+ "<span role='heading' aria-level='1'></span>",
+ "<span role='combobox' aria-controls='true' aria-expanded='true'></span>",
+ ],
+ {category: "lint/jsx-a11y/roleHasRequiredAriaProps"},
+ );
+ },
+);

ADD
packages/@romejs/compiler/lint/rules/jsx-a11y/roleHasRequiredAriaProps.ts
+import {Path, TransformExitResult} from "@romejs/compiler";
+import {descriptions} from "@romejs/diagnostics";
+import {
+ getJSXAttribute,
+ hasJSXAttribute,
+ isJSXElement,
+} from "@romejs/js-ast-utils";
+import {roles} from "@romejs/compiler/lint/rules/ariaHelpers";
+
+export default {
+ name: "jsxA11YRoleHasRequiredAriaProps",
+ enter(path: Path): TransformExitResult {
+ const {node} = path;
+
+ if (isJSXElement(node) && hasJSXAttribute(node, "role")) {
+ const attr = getJSXAttribute(node, "role");
+ if (attr && attr.value && attr.value.type === "JSStringLiteral") {
+ const role = roles.get(attr.value.value);
+ if (role) {
+ const hasAllProps = role.requiredProps.every((prop) => {
+ return hasJSXAttribute(node, prop);
+ });
+
+ if (!hasAllProps) {
+ const missingProps = role.requiredProps.map((prop) => {
+ const attr = getJSXAttribute(node, prop);
+ if (!attr) {
+ return prop;
+ }
+ return "";
+ }).filter(Boolean);
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_A11Y_ROLE_HAS_REQUIRED_ARIA_PROPS(
+ attr.value.value,
+ missingProps,
+ ),
+ );
+ }
+ }
+ }
+ }
+
+ return node;
+ },
+};

MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -158,6 +158,7 @@ type LintDiagnosticCategory =
| "lint/jsx-a11y/noDistractingElements"
| "lint/jsx-a11y/noOnChange"
| "lint/jsx-a11y/noTargetBlank"
+ | "lint/jsx-a11y/roleHasRequiredAriaProps"
| "lint/jsx-a11y/scope"
| "lint/jsx-a11y/tabindexNoPositive"
| "lint/react/buttonHasType"

MODIFY
packages/@romejs/diagnostics/descriptions/lint.ts
packages/@romejs/diagnostics/descriptions/lint.ts
@@ -296,6 +296,20 @@ export const lint = createDiagnosticsCategory({
},
],
},
+ JSX_A11Y_ROLE_HAS_REQUIRED_ARIA_PROPS: (
+ roleName: string,
+ missingAttributes: Array<string>,
+ ) => ({
+ category: "lint/jsx-a11y/roleHasRequiredAriaProps",
+ message: `The element with the <emphasis>${roleName}</emphasis> ARIA role does not have the required ARIA attributes.`,
+ advice: missingAttributes.map((missingAttribute) => {
+ return {
+ type: "log",
+ category: "info",
+ text: `Missing aria attribute: ${missingAttribute}`,
+ };
+ }),
+ }),
REACT_JSX_KEY: (origin: string) => ({
category: "lint/react/jsxKey",
message: `Provide a <emphasis>key</emphasis> prop with a unique value for each element in <emphasis>${origin}</emphasis>.`,

rule: role-has-required-aria-props (#499)