144 lines
4.8 KiB
TypeScript
144 lines
4.8 KiB
TypeScript
import type { DevToolbarRule } from "./types.ts";
|
|
import { accessibleName, createIssue } from "./helpers.ts";
|
|
|
|
export const accessibilityRules: DevToolbarRule[] = [
|
|
{
|
|
id: "a11y/image-alt",
|
|
category: "accessibility",
|
|
defaultSeverity: "error",
|
|
description: "Images need alternative text.",
|
|
run: ({ root }) =>
|
|
[...root.querySelectorAll("img:not([alt])")].map((element) =>
|
|
createIssue({
|
|
ruleId: "a11y/image-alt",
|
|
category: "accessibility",
|
|
severity: "error",
|
|
title: "Image is missing alt text",
|
|
message: 'Add alt text for meaningful images, or alt="" for decorative images.',
|
|
element,
|
|
recommendation: "Describe the image's purpose in a concise alt attribute.",
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
id: "a11y/control-name",
|
|
category: "accessibility",
|
|
defaultSeverity: "error",
|
|
description: "Interactive controls need accessible names.",
|
|
run: ({ root }) =>
|
|
[...root.querySelectorAll("button, a[href], [role='button']")]
|
|
.filter((element) => !accessibleName(element))
|
|
.map((element) =>
|
|
createIssue({
|
|
ruleId: "a11y/control-name",
|
|
category: "accessibility",
|
|
severity: "error",
|
|
title: "Control has no accessible name",
|
|
message: "Screen readers cannot identify this control.",
|
|
element,
|
|
recommendation: "Add visible text, aria-label, or aria-labelledby.",
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
id: "a11y/form-label",
|
|
category: "accessibility",
|
|
defaultSeverity: "error",
|
|
description: "Form controls need labels.",
|
|
run: ({ root }) =>
|
|
[...root.querySelectorAll("input:not([type='hidden']), select, textarea")]
|
|
.filter((element) => {
|
|
const id = element.id;
|
|
return (
|
|
!element.getAttribute("aria-label") &&
|
|
!element.getAttribute("aria-labelledby") &&
|
|
!(id && root.querySelector(`label[for='${CSS.escape(id)}']`)) &&
|
|
!element.closest("label")
|
|
);
|
|
})
|
|
.map((element) =>
|
|
createIssue({
|
|
ruleId: "a11y/form-label",
|
|
category: "accessibility",
|
|
severity: "error",
|
|
title: "Form control has no label",
|
|
message: "Users of assistive technology may not know what this field is for.",
|
|
element,
|
|
recommendation: "Associate a label using for/id or aria-labelledby.",
|
|
}),
|
|
),
|
|
},
|
|
{
|
|
id: "a11y/duplicate-id",
|
|
category: "accessibility",
|
|
defaultSeverity: "error",
|
|
description: "IDs must be unique.",
|
|
run: ({ root }) => {
|
|
const seen = new Set<string>();
|
|
const duplicates: Element[] = [];
|
|
for (const element of root.querySelectorAll("[id]")) {
|
|
if (seen.has(element.id)) duplicates.push(element);
|
|
else seen.add(element.id);
|
|
}
|
|
return duplicates.map((element) =>
|
|
createIssue({
|
|
ruleId: "a11y/duplicate-id",
|
|
category: "accessibility",
|
|
severity: "error",
|
|
title: "Duplicate element ID",
|
|
message: `The ID “${element.id}” is used more than once.`,
|
|
element,
|
|
recommendation: "Use a unique ID for every element.",
|
|
}),
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "a11y/heading-order",
|
|
category: "accessibility",
|
|
defaultSeverity: "warning",
|
|
description: "Heading levels should not skip.",
|
|
run: ({ root }) => {
|
|
const issues = [];
|
|
let previous = 0;
|
|
for (const element of root.querySelectorAll("h1,h2,h3,h4,h5,h6")) {
|
|
const level = Number(element.tagName.slice(1));
|
|
if (previous && level > previous + 1)
|
|
issues.push(
|
|
createIssue({
|
|
ruleId: "a11y/heading-order",
|
|
category: "accessibility",
|
|
severity: "warning",
|
|
title: "Heading level is skipped",
|
|
message: `Heading jumps from h${previous} to h${level}.`,
|
|
element,
|
|
recommendation: "Use headings in a logical hierarchy.",
|
|
}),
|
|
);
|
|
previous = level;
|
|
}
|
|
return issues;
|
|
},
|
|
},
|
|
{
|
|
id: "a11y/positive-tabindex",
|
|
category: "accessibility",
|
|
defaultSeverity: "warning",
|
|
description: "Positive tabindex disrupts natural keyboard order.",
|
|
run: ({ root }) =>
|
|
[...root.querySelectorAll("[tabindex]")]
|
|
.filter((el) => Number(el.getAttribute("tabindex")) > 0)
|
|
.map((element) =>
|
|
createIssue({
|
|
ruleId: "a11y/positive-tabindex",
|
|
category: "accessibility",
|
|
severity: "warning",
|
|
title: "Positive tabindex used",
|
|
message: "Positive tabindex values create an unexpected keyboard focus order.",
|
|
element,
|
|
recommendation: 'Use tabindex="0" or rely on native document order.',
|
|
}),
|
|
),
|
|
},
|
|
];
|