feat(lint): useHeadingContent for HTML
ADD
internal/compiler/lint/rules/a11y/useHeadingContent.test.rjson
+[
+ {
+ filename: "file.tsx"
+ invalid: [
+ "<h1 />"
+ "<h1><TextWrapper aria-hidden /></h1>"
+ "<h1><div aria-hidden /></h1>"
+ ]
+ valid: [
+ "<h1>heading</h1>"
+ "<h1><TextWrapper /></h1>"
+ "<h1 dangerouslySetInnerHTML={{ __html: 'heading' }} />"
+ "<h1><div aria-hidden />visible content</h1>"
+ ]
+}
+ {
+ filename: "file.html"
+ invalid: [
+ "<h1></h1>"
+ ]
+ valid: [
+ "<h1>heading</h1>"
+ "<h1><div aria-hidden=\"true\"></div>visible content</h1>"
+ ]
+}
+]

RENAME
internal/compiler/lint/rules/jsx-a11y/useHeadingContent.ts
internal/compiler/lint/rules/a11y/useHeadingContent.ts
import {createVisitor, signals} from "@internal/compiler";
import {descriptions} from "@internal/diagnostics";
import {hasJSXAttribute, isJSXElement} from "@internal/js-ast-utils";
-import {JSXElement} from "@internal/ast";
+import {HTMLElement, JSXElement} from "@internal/ast";
+import isHTMLElement from "@internal/js-ast-utils/isHTMLElement";
+import hasHTMLAttribute from "@internal/js-ast-utils/hasHTMLAttribute";
const HEADINGS = ["h1", "h2", "h3", "h4", "h5", "h6"];
@@ -18,18 +20,31 @@ function hasHeadingContent(node: JSXElement): boolean {
);
}
+function htmlHasHeadingContent(node: HTMLElement): boolean {
+ return node.children.some((child) =>
+ child.type !== "HTMLElement" || !hasHTMLAttribute(child, "aria-hidden")
+ );
+}
+
export default createVisitor({
- name: "jsx-a11y/useHeadingContent",
+ name: "a11y/useHeadingContent",
enter(path) {
const {node} = path;
- if (
+ if (isHTMLElement(node) && HEADINGS.includes(node.name.name)) {
+ if (!htmlHasHeadingContent(node)) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.A11_Y_USE_HEADING_CONTENT,
+ );
+ }
+ } else if (
HEADINGS.some((heading) => isJSXElement(node, heading)) &&
!hasHeadingContent(node as JSXElement)
) {
path.context.addNodeDiagnostic(
node,
- descriptions.LINT.JSX_A11Y_HEADING_USE_CONTENT,
+ descriptions.LINT.A11_Y_USE_HEADING_CONTENT,
);
}

MODIFY
internal/compiler/lint/rules/index.ts
internal/compiler/lint/rules/index.ts
-/* GENERATED:START(hash:16cbac848efd4603c87f0868b93ae6e7ebc0ec58,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:bf542a4bd5d87134d9ccf43918f6537fa9dbad88,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
import noAccessKey from "./a11y/noAccessKey";
import noAriaUnsupportedElements from "./a11y/noAriaUnsupportedElements";
import noAutofocus from "./a11y/noAutofocus";
@@ -15,6 +15,7 @@ import useAltText from "./a11y/useAltText";
import useAnchorContent from "./a11y/useAnchorContent";
import useAriaProps from "./a11y/useAriaProps";
import useAriaProptypes from "./a11y/useAriaProptypes";
+import useHeadingContent from "./a11y/useHeadingContent";
import useHtmlLang from "./a11y/useHtmlLang";
import useIframeTitle from "./a11y/useIframeTitle";
import useKeyWithClickEvents from "./a11y/useKeyWithClickEvents";
@@ -74,7 +75,6 @@ import useTemplate from "./js/useTemplate";
import useWhile from "./js/useWhile";
import noRedundantRoles from "./jsx-a11y/noRedundantRoles";
import useAriaPropsForRole from "./jsx-a11y/useAriaPropsForRole";
-import useHeadingContent from "./jsx-a11y/useHeadingContent";
import useValidAnchor from "./jsx-a11y/useValidAnchor";
import noCommentText from "./jsx/noCommentText";
import noDuplicateProps from "./jsx/noDuplicateProps";
@@ -138,6 +138,7 @@ lintTransforms.set("a11y/useAltText", useAltText);
lintTransforms.set("a11y/useAnchorContent", useAnchorContent);
lintTransforms.set("a11y/useAriaProps", useAriaProps);
lintTransforms.set("a11y/useAriaProptypes", useAriaProptypes);
+lintTransforms.set("a11y/useHeadingContent", useHeadingContent);
lintTransforms.set("a11y/useHtmlLang", useHtmlLang);
lintTransforms.set("a11y/useIframeTitle", useIframeTitle);
lintTransforms.set("a11y/useKeyWithClickEvents", useKeyWithClickEvents);
@@ -203,7 +204,6 @@ lintTransforms.set("js/useTemplate", useTemplate);
lintTransforms.set("js/useWhile", useWhile);
lintTransforms.set("jsx-a11y/noRedundantRoles", noRedundantRoles);
lintTransforms.set("jsx-a11y/useAriaPropsForRole", useAriaPropsForRole);
-lintTransforms.set("jsx-a11y/useHeadingContent", useHeadingContent);
lintTransforms.set("jsx-a11y/useValidAnchor", useValidAnchor);
lintTransforms.set("jsx/noCommentText", noCommentText);
lintTransforms.set("jsx/noDuplicateProps", noDuplicateProps);
@@ -284,6 +284,7 @@ export const lintRuleNames: LintRuleName[] = [
"a11y/useAnchorContent",
"a11y/useAriaProps",
"a11y/useAriaProptypes",
+ "a11y/useHeadingContent",
"a11y/useHtmlLang",
"a11y/useIframeTitle",
"a11y/useKeyWithClickEvents",
@@ -343,7 +344,6 @@ export const lintRuleNames: LintRuleName[] = [
"js/useWhile",
"jsx-a11y/noRedundantRoles",
"jsx-a11y/useAriaPropsForRole",
- "jsx-a11y/useHeadingContent",
"jsx-a11y/useValidAnchor",
"jsx/noCommentText",
"jsx/noDuplicateProps",
@@ -404,6 +404,7 @@ export type LintRuleName =
| "a11y/useAnchorContent"
| "a11y/useAriaProps"
| "a11y/useAriaProptypes"
+ | "a11y/useHeadingContent"
| "a11y/useHtmlLang"
| "a11y/useIframeTitle"
| "a11y/useKeyWithClickEvents"
@@ -463,7 +464,6 @@ export type LintRuleName =
| "js/useWhile"
| "jsx-a11y/noRedundantRoles"
| "jsx-a11y/useAriaPropsForRole"
- | "jsx-a11y/useHeadingContent"
| "jsx-a11y/useValidAnchor"
| "jsx/noCommentText"
| "jsx/noDuplicateProps"

MODIFY
internal/compiler/lint/rules/jsx-a11y/noRedundantRoles.test.rjson
internal/compiler/lint/rules/jsx-a11y/noRedundantRoles.test.rjson
@@ -2,8 +2,8 @@ filename: "file.tsx"
invalid: [
"<article role='article'></article>"
"<button role='button'></button>"
- "<h1 role='heading' aria-level='1'></h1>"
- "<h1 role='heading'></h1>"
+ "<h1 role='heading' aria-level='1'>title</h1>"
+ "<h1 role='heading'>title</h1>"
"<dialog role='dialog'></dialog>"
"<input type='checkbox' role='checkbox' />"
"<figure role='figure'></figure>"

DELETE
internal/compiler/lint/rules/jsx-a11y/useHeadingContent.test.rjson
-filename: "file.tsx"
-invalid: [
- "<h1 />"
- "<h1><TextWrapper aria-hidden /></h1>"
- "<h1><div aria-hidden /></h1>"
-]
-valid: [
- "<h1>heading</h1>"
- "<h1><TextWrapper /></h1>"
- "<h1 dangerouslySetInnerHTML={{ __html: 'heading' }} />"
- "<h1><div aria-hidden />visible content</h1>"
-]

MODIFY
internal/compiler/lint/rules/tests.ts
internal/compiler/lint/rules/tests.ts
@@ -12,7 +12,7 @@ type Tests = Dict<{
cases: Test | Test[] | Test[][];
}>;
-/* GENERATED:START(hash:01ad0c80f33dc931c1316888270304754d8dad2e,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:1e7ee1bcd338286eb881f6d6bc738b39890e2617,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
// @ts-ignore
import noAccessKey from "./a11y/noAccessKey.test.rjson";
// @ts-ignore
@@ -46,6 +46,8 @@ import useAriaProps from "./a11y/useAriaProps.test.rjson";
// @ts-ignore
import useAriaProptypes from "./a11y/useAriaProptypes.test.rjson";
// @ts-ignore
+import useHeadingContent from "./a11y/useHeadingContent.test.rjson";
+// @ts-ignore
import useHtmlLang from "./a11y/useHtmlLang.test.rjson";
// @ts-ignore
import useIframeTitle from "./a11y/useIframeTitle.test.rjson";
@@ -162,8 +164,6 @@ import noRedundantRoles from "./jsx-a11y/noRedundantRoles.test.rjson";
// @ts-ignore
import useAriaPropsForRole from "./jsx-a11y/useAriaPropsForRole.test.rjson";
// @ts-ignore
-import useHeadingContent from "./jsx-a11y/useHeadingContent.test.rjson";
-// @ts-ignore
import useValidAnchor from "./jsx-a11y/useValidAnchor.test.rjson";
// @ts-ignore
import noCommentText from "./jsx/noCommentText.test.rjson";
@@ -303,6 +303,10 @@ export const tests: Tests = {
category: ["lint", "a11y", "useAriaProptypes"],
cases: useAriaProptypes,
},
+ "a11y/useHeadingContent": {
+ category: ["lint", "a11y", "useHeadingContent"],
+ cases: useHeadingContent,
+ },
"a11y/useHtmlLang": {
category: ["lint", "a11y", "useHtmlLang"],
cases: useHtmlLang,
@@ -535,10 +539,6 @@ export const tests: Tests = {
category: ["lint", "jsx-a11y", "useAriaPropsForRole"],
cases: useAriaPropsForRole,
},
- "jsx-a11y/useHeadingContent": {
- category: ["lint", "jsx-a11y", "useHeadingContent"],
- cases: useHeadingContent,
- },
"jsx-a11y/useValidAnchor": {
category: ["lint", "jsx-a11y", "useValidAnchor"],
cases: useValidAnchor,

MODIFY
internal/diagnostics/categories.ts
internal/diagnostics/categories.ts
@@ -103,7 +103,7 @@ type DiagnosticCategoryString = StringConverter<DiagnosticCategory>;
type DiagnosticLintCategoryString = StringConverter<DiagnosticLintCategory>;
-/* GENERATED:START(hash:1dceb67feb310036105512830eb9f1d09faf1bb6,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:74f0d309f1c69b69d915843a8b2ba7a7509c9d94,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
export type DiagnosticLintCategory =
| ["lint", "a11y", "noAccessKey"]
| ["lint", "a11y", "noAriaUnsupportedElements"]
@@ -121,6 +121,7 @@ export type DiagnosticLintCategory =
| ["lint", "a11y", "useAnchorContent"]
| ["lint", "a11y", "useAriaProps"]
| ["lint", "a11y", "useAriaProptypes"]
+ | ["lint", "a11y", "useHeadingContent"]
| ["lint", "a11y", "useHtmlLang"]
| ["lint", "a11y", "useIframeTitle"]
| ["lint", "a11y", "useKeyWithClickEvents"]
@@ -180,7 +181,6 @@ export type DiagnosticLintCategory =
| ["lint", "js", "useWhile"]
| ["lint", "jsx-a11y", "noRedundantRoles"]
| ["lint", "jsx-a11y", "useAriaPropsForRole"]
- | ["lint", "jsx-a11y", "useHeadingContent"]
| ["lint", "jsx-a11y", "useValidAnchor"]
| ["lint", "jsx", "noCommentText"]
| ["lint", "jsx", "noDuplicateProps"]
@@ -253,6 +253,7 @@ const lintCategoryNameMap: {
"lint/a11y/useAnchorContent": ["lint", "a11y", "useAnchorContent"],
"lint/a11y/useAriaProps": ["lint", "a11y", "useAriaProps"],
"lint/a11y/useAriaProptypes": ["lint", "a11y", "useAriaProptypes"],
+ "lint/a11y/useHeadingContent": ["lint", "a11y", "useHeadingContent"],
"lint/a11y/useHtmlLang": ["lint", "a11y", "useHtmlLang"],
"lint/a11y/useIframeTitle": ["lint", "a11y", "useIframeTitle"],
"lint/a11y/useKeyWithClickEvents": ["lint", "a11y", "useKeyWithClickEvents"],
@@ -328,7 +329,6 @@ const lintCategoryNameMap: {
"jsx-a11y",
"useAriaPropsForRole",
],
- "lint/jsx-a11y/useHeadingContent": ["lint", "jsx-a11y", "useHeadingContent"],
"lint/jsx-a11y/useValidAnchor": ["lint", "jsx-a11y", "useValidAnchor"],
"lint/jsx/noCommentText": ["lint", "jsx", "noCommentText"],
"lint/jsx/noDuplicateProps": ["lint", "jsx", "noDuplicateProps"],

MODIFY
internal/diagnostics/descriptions/lint.ts
internal/diagnostics/descriptions/lint.ts
@@ -17,6 +17,17 @@ import {addEmphasis, createDiagnosticsCategory, orJoin} from "./index";
import {DIAGNOSTIC_CATEGORIES} from "../categories";
export const lint = createDiagnosticsCategory({
+ A11_Y_USE_HEADING_CONTENT: {
+ category: DIAGNOSTIC_CATEGORIES["lint/a11y/useHeadingContent"],
+ message: markup`Provide screen reader accessible content when using <emphasis>heading</emphasis> elements.`,
+ advice: [
+ {
+ type: "log",
+ category: "info",
+ text: markup`All headings on a page should have content that is accessible to screen readers.`,
+ },
+ ],
+ },
A11_Y_NO_ON_CHANGE: (goodEventName: string, badEventName: string) => ({
category: DIAGNOSTIC_CATEGORIES["lint/a11y/noOnChange"],
message: markup`Provide an <emphasis>${goodEventName}</emphasis> event instead of an <emphasis>${badEventName}</emphasis> event unless absolutely necessary.`,
@@ -483,17 +494,6 @@ export const lint = createDiagnosticsCategory({
},
],
},
- JSX_A11Y_HEADING_USE_CONTENT: {
- category: DIAGNOSTIC_CATEGORIES["lint/jsx-a11y/useHeadingContent"],
- message: markup`Provide screen reader accessible content when using <emphasis>heading</emphasis> elements.`,
- advice: [
- {
- type: "log",
- category: "info",
- text: markup`All headings on a page should have content that is accessible to screen readers.`,
- },
- ],
- },
A11Y_HTML_USE_LANG: {
category: DIAGNOSTIC_CATEGORIES["lint/a11y/useHtmlLang"],
message: markup`Provide a <emphasis>lang</emphasis> attribute when using the <emphasis>html</emphasis> element.`,

MODIFY
website/src/docs/lint/rules/index.md
website/src/docs/lint/rules/index.md
@@ -11,7 +11,7 @@ eleventyNavigation:
# Rules
-<!-- GENERATED:START(hash:5942555258e3e3b0b1cbf08824683f0338e1efaf,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. -->
+<!-- GENERATED:START(hash:25ad3a6dc25d79c2f904f880a5d7dc2118a5c47c,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. -->
<section>
<h2>JavaScript</h2>
<p>Rule semantics and descriptions taken from <a href="https://eslint.org/">ESLint</a>. See individual rule docs for direct references.</p>
@@ -500,6 +500,13 @@ enforce all `aria-*` props are valid
enforce ARIA state and property values are valid
</div>
<div class="rule">
+<h3 data-toc-exclude id="useHeadingContent">
+ <a href="/docs/lint/rules/a11y/useHeadingContent">useHeadingContent</a>
+ <a class="header-anchor" href="#useHeadingContent"></a>
+</h3>
+MISSING DOCUMENTATION
+</div>
+<div class="rule">
<h3 data-toc-exclude id="useHtmlLang">
<a href="/docs/lint/rules/a11y/useHtmlLang">useHtmlLang</a>
<a class="header-anchor" href="#useHtmlLang"></a>
@@ -560,13 +567,6 @@ enforce explicit role property is not the same as implicit/default role property
enforce that elements with ARIA roles must have all required attributes for that role
</div>
<div class="rule">
-<h3 data-toc-exclude id="useHeadingContent">
- <a href="/docs/lint/rules/jsx-a11y/useHeadingContent">useHeadingContent</a>
- <a class="header-anchor" href="#useHeadingContent"></a>
-</h3>
-enforce heading (`h1`, `h2`, etc) elements contain accessible content
-</div>
-<div class="rule">
<h3 data-toc-exclude id="useValidAnchor">
<a href="/docs/lint/rules/jsx-a11y/useValidAnchor">useValidAnchor</a>
<a class="header-anchor" href="#useValidAnchor"></a>

feat(lint): useHeadingContent for HTML (#1469)