add ariaSupportedElements rule and tests
ADD
packages/@romejs/compiler/lint/rules/aria-helpers.ts
+export type MapOfARIAPropertyDefinitions = Map<
+ ARIAProperty,
+ ARIAPropertyDefinition
+>;
+
+export type ARIAProperty =
+ | "aria-activedescendant"
+ | "aria-atomic"
+ | "aria-autocomplete"
+ | "aria-colcount"
+ | "aria-colindex"
+ | "aria-colspan"
+ | "aria-controls"
+ | "aria-current"
+ | "aria-describedby"
+ | "aria-details"
+ | "aria-dropeffect"
+ | "aria-errormessage"
+ | "aria-flowto"
+ | "aria-haspopup"
+ | "aria-keyshortcuts"
+ | "aria-label"
+ | "aria-labelledby"
+ | "aria-level"
+ | "aria-live"
+ | "aria-modal"
+ | "aria-multiline"
+ | "aria-multiselectable"
+ | "aria-orientation"
+ | "aria-owns"
+ | "aria-placeholder"
+ | "aria-posinset"
+ | "aria-readonly"
+ | "aria-relevant"
+ | "aria-required"
+ | "aria-roledescription"
+ | "aria-rowcount"
+ | "aria-rowindex"
+ | "aria-rowspan"
+ | "aria-setsize"
+ | "aria-sort"
+ | "aria-valuemax"
+ | "aria-valuemin"
+ | "aria-valuenow"
+ | "aria-valuetext"
+ | ARIAState;
+
+export type ARIAState =
+ | "aria-busy"
+ | "aria-checked"
+ | "aria-disabled"
+ | "aria-expanded"
+ | "aria-grabbed"
+ | "aria-hidden"
+ | "aria-invalid"
+ | "aria-pressed"
+ | "aria-selected";
+
+export type ARIAPropertyDefinition = {
+ type:
+ | "string"
+ | "id"
+ | "idlist"
+ | "integer"
+ | "number"
+ | "boolean"
+ | "token"
+ | "tokenlist"
+ | "tristate";
+ value?: Array<string | boolean>;
+ allowundefined?: boolean;
+};
+
+export const ariaPropsMap: MapOfARIAPropertyDefinitions = new Map([
+ [
+ "aria-activedescendant",
+ {
+ "type": "id",
+ },
+ ],
+ [
+ "aria-atomic",
+ {
+ "type": "boolean",
+ },
+ ],
+ [
+ "aria-autocomplete",
+ {
+ "type": "token",
+ "values": ["inline", "list", "both", "none"],
+ },
+ ],
+ [
+ "aria-busy",
+ {
+ "type": "boolean",
+ },
+ ],
+ [
+ "aria-checked",
+ {
+ "type": "tristate",
+ },
+ ],
+ [
+ "aria-colcount",
+ {
+ type: "integer",
+ },
+ ],
+ [
+ "aria-colindex",
+ {
+ type: "integer",
+ },
+ ],
+ [
+ "aria-colspan",
+ {
+ type: "integer",
+ },
+ ],
+ [
+ "aria-controls",
+ {
+ "type": "idlist",
+ },
+ ],
+ [
+ "aria-current",
+ {
+ type: "token",
+ values: ["page", "step", "location", "date", "time", true, false],
+ },
+ ],
+ [
+ "aria-describedby",
+ {
+ "type": "idlist",
+ },
+ ],
+ [
+ "aria-details",
+ {
+ "type": "id",
+ },
+ ],
+ [
+ "aria-disabled",
+ {
+ "type": "boolean",
+ },
+ ],
+ [
+ "aria-dropeffect",
+ {
+ "type": "tokenlist",
+ "values": ["copy", "execute", "link", "move", "none", "popup"],
+ },
+ ],
+ [
+ "aria-errormessage",
+ {
+ "type": "id",
+ },
+ ],
+ [
+ "aria-expanded",
+ {
+ "type": "boolean",
+ "allowundefined": true,
+ },
+ ],
+ [
+ "aria-flowto",
+ {
+ "type": "idlist",
+ },
+ ],
+ [
+ "aria-grabbed",
+ {
+ "type": "boolean",
+ "allowundefined": true,
+ },
+ ],
+ [
+ "aria-haspopup",
+ {
+ "type": "token",
+ "values": [false, true, "menu", "listbox", "tree", "grid", "dialog"],
+ },
+ ],
+ [
+ "aria-hidden",
+ {
+ "type": "boolean",
+ "allowundefined": true,
+ },
+ ],
+ [
+ "aria-invalid",
+ {
+ "type": "token",
+ "values": ["grammar", false, "spelling", true],
+ },
+ ],
+ [
+ "aria-keyshortcuts",
+ {
+ type: "string",
+ },
+ ],
+ [
+ "aria-label",
+ {
+ "type": "string",
+ },
+ ],
+ [
+ "aria-labelledby",
+ {
+ "type": "idlist",
+ },
+ ],
+ [
+ "aria-level",
+ {
+ "type": "integer",
+ },
+ ],
+ [
+ "aria-live",
+ {
+ "type": "token",
+ "values": ["assertive", "off", "polite"],
+ },
+ ],
+ [
+ "aria-modal",
+ {
+ type: "boolean",
+ },
+ ],
+ [
+ "aria-multiline",
+ {
+ "type": "boolean",
+ },
+ ],
+ [
+ "aria-multiselectable",
+ {
+ "type": "boolean",
+ },
+ ],
+ [
+ "aria-orientation",
+ {
+ "type": "token",
+ "values": ["vertical", "undefined", "horizontal"],
+ },
+ ],
+ [
+ "aria-owns",
+ {
+ "type": "idlist",
+ },
+ ],
+ [
+ "aria-placeholder",
+ {
+ type: "string",
+ },
+ ],
+ [
+ "aria-posinset",
+ {
+ "type": "integer",
+ },
+ ],
+ [
+ "aria-pressed",
+ {
+ "type": "tristate",
+ },
+ ],
+ [
+ "aria-readonly",
+ {
+ "type": "boolean",
+ },
+ ],
+ [
+ "aria-relevant",
+ {
+ "type": "tokenlist",
+ "values": ["additions", "all", "removals", "text"],
+ },
+ ],
+ [
+ "aria-required",
+ {
+ "type": "boolean",
+ },
+ ],
+ [
+ "aria-roledescription",
+ {
+ type: "string",
+ },
+ ],
+ [
+ "aria-rowcount",
+ {
+ type: "integer",
+ },
+ ],
+ [
+ "aria-rowindex",
+ {
+ type: "integer",
+ },
+ ],
+ [
+ "aria-rowspan",
+ {
+ type: "integer",
+ },
+ ],
+ [
+ "aria-selected",
+ {
+ "type": "boolean",
+ "allowundefined": true,
+ },
+ ],
+ [
+ "aria-setsize",
+ {
+ "type": "integer",
+ },
+ ],
+ [
+ "aria-sort",
+ {
+ "type": "token",
+ "values": ["ascending", "descending", "none", "other"],
+ },
+ ],
+ [
+ "aria-valuemax",
+ {
+ "type": "number",
+ },
+ ],
+ [
+ "aria-valuemin",
+ {
+ "type": "number",
+ },
+ ],
+ [
+ "aria-valuenow",
+ {
+ "type": "number",
+ },
+ ],
+ [
+ "aria-valuetext",
+ {
+ "type": "string",
+ },
+ ],
+]);

MODIFY
packages/@romejs/compiler/lint/rules/index.ts
packages/@romejs/compiler/lint/rules/index.ts
@@ -57,6 +57,7 @@ import unsafeNegation from "./js/unsafeNegation";
import unusedVariables from "./js/unusedVariables";
import altText from "./jsx-a11y/altText";
import anchorHasContent from "./jsx-a11y/anchorHasContent";
+import ariaUnsupportedElements from "./jsx-a11y/ariaUnsupportedElements";
import headingHasContent from "./jsx-a11y/headingHasContent";
import htmlHasLang from "./jsx-a11y/htmlHasLang";
import iframeHasTitle from "./jsx-a11y/iframeHasTitle";
@@ -140,6 +141,7 @@ export const lintTransforms = [
unusedVariables,
altText,
anchorHasContent,
+ ariaUnsupportedElements,
headingHasContent,
htmlHasLang,
iframeHasTitle,

ADD
packages/@romejs/compiler/lint/rules/jsx-a11y/ariaUnsupportedElements.test.ts
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ "aria unsupported elements",
+ async (t) => {
+ await testLintMultiple(
+ t,
+ [
+ // INVALID
+ "<meta charset='UTF-8' aria-hidden='false' />",
+ "<meta charset='UTF-8' role='meta' />",
+ "<html aria-required='true' />",
+ "<html role='html'></html>",
+ "<script aria-label='script'></script>",
+ "<script role='script'></script>",
+ "<style aria-labelledby></style>",
+ "<style role='style'></style>",
+ // VALID
+ "<meta charset='UTF-8' />",
+ "<html maria='text'></html>",
+ "<script></script>",
+ "<style parole></style>",
+ ],
+ {category: "lint/jsx-a11y/ariaUnsupportedElements"},
+ );
+ },
+);

ADD
packages/@romejs/compiler/lint/rules/jsx-a11y/ariaUnsupportedElements.ts
+import {Path} from "@romejs/compiler";
+import {TransformExitResult} from "@romejs/compiler/types";
+import {descriptions} from "@romejs/diagnostics";
+import {JSXElement} from "@romejs/ast";
+import {hasJSXAttribute, isJSXElement} from "@romejs/js-ast-utils";
+import {ARIAProperty, ariaPropsMap} from "../aria-helpers";
+
+function hasAriaAttributes(node: JSXElement): boolean {
+ const hasRole = hasJSXAttribute(node, "role");
+
+ return (
+ hasRole ||
+ node.attributes.some((attr) =>
+ attr.type === "JSXAttribute" &&
+ attr.name.type === "JSXIdentifier" &&
+ ariaPropsMap.has((attr.name.name as ARIAProperty))
+ )
+ );
+}
+
+export default {
+ name: "ariaUnsupportedElements",
+ enter(path: Path): TransformExitResult {
+ const {node} = path;
+
+ if (
+ !(isJSXElement(node, "meta") ||
+ isJSXElement(node, "html") ||
+ isJSXElement(node, "script") ||
+ isJSXElement(node, "style"))
+ ) {
+ return node;
+ }
+
+ if (hasAriaAttributes(node)) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_A11Y_ARIA_UNSUPPORTED_ELEMENTS,
+ );
+ }
+
+ return node;
+ },
+};

MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -143,6 +143,7 @@ type LintDiagnosticCategory =
| "lint/js/unusedVariables"
| "lint/jsx-a11y/altText"
| "lint/jsx-a11y/anchorHasContent"
+ | "lint/jsx-a11y/ariaUnsupportedElements"
| "lint/jsx-a11y/headingHasContent"
| "lint/jsx-a11y/htmlHasLang"
| "lint/jsx-a11y/iframeHasTitle"

MODIFY
packages/@romejs/diagnostics/descriptions/lint.ts
packages/@romejs/diagnostics/descriptions/lint.ts
@@ -53,6 +53,10 @@ export const lint = createDiagnosticsCategory({
category: "lint/react/noWillUpdateSetState",
message: "Avoid <emphasis>this.setState</emphasis> in <emphasis>componentWillUpdate</emphasis>",
},
+ JSX_A11Y_ARIA_UNSUPPORTED_ELEMENTS: {
+ category: "lint/jsx-a11y/ariaUnsupportedElements",
+ message: "Avoid <emphasis>role</emphasis> and <emphasis>aria-* props</emphasis> on <emphasis>meta</emphasis>, <emphasis>html</emphasis>, <emphasis>script</emphasis>, and <emphasis>style</emphasis> elements.",
+ },
JSX_A11Y_ANCHOR_HAS_CONTENT: {
category: "lint/jsx-a11y/anchorHasContent",
message: "Anchor must have content and the content must be accessible by a screen reader.",

add ariaSupportedElements rule and tests (#481)