@html-eslint/eslint-plugin
Version:
ESLint plugin for HTML
296 lines (277 loc) • 5.15 kB
JavaScript
/** @import {RuleModule} from "../types" */
const { NODE_TYPES } = require("@html-eslint/parser");
const { RULE_CATEGORY } = require("../constants");
const { findAttr } = require("./utils/node");
const { createVisitors } = require("./utils/visitors");
const { getRuleUrl } = require("./utils/rule");
const MESSAGE_IDS = {
INVALID: "invalid",
INVALID_PRESENTATION: "invalidPresentation",
};
/**
* Elements and role attribute constants are taken from ember-template-lint.
* https://github.com/ember-template-lint/ember-template-lint/blob/master/lib/rules/no-invalid-role.js
*/
// https://www.w3.org/TR/wai-aria/#document_structure_roles
const DOCUMENT_STRUCTURE_ROLES = new Set([
"application",
"article",
"associationlist",
"associationlistitemkey",
"associationlistitemvalue",
"blockquote",
"caption",
"cell",
"code",
"columnheader",
"comment",
"definition",
"deletion",
"directory",
"document",
"emphasis",
"feed",
"figure",
"generic",
"group",
"heading",
"img",
"insertion",
"list",
"listitem",
"mark",
"math",
"meter",
"none",
"note",
"paragraph",
"presentation",
"row",
"rowgroup",
"rowheader",
"separator", // When not focusable
"strong",
"subscript",
"suggestion",
"superscript",
"table",
"term",
"time",
"toolbar",
"tooltip",
]);
// https://www.w3.org/TR/wai-aria/#widget_roles
const WIDGET_ROLES = new Set([
"button",
"checkbox",
"gridcell",
"link",
"menuitem",
"menuitemcheckbox",
"menuitemradio",
"option",
"progressbar",
"radio",
"scrollbar",
"searchbox",
"separator", // When focusable
"slider",
"spinbutton",
"switch",
"tab",
"tabpanel",
"textbox",
"treeitem",
]);
const COMPOSITE_WIDGET_ROLES = new Set([
"combobox",
"grid",
"listbox",
"menu",
"menubar",
"radiogroup",
"tablist",
"tree",
"treegrid",
]);
// https://www.w3.org/TR/wai-aria/#landmark_roles
const LANDMARK_ROLES = new Set([
"banner",
"complementary",
"contentinfo",
"form",
"main",
"navigation",
"region",
"search",
]);
// https://www.w3.org/TR/wai-aria/#live_region_roles
const LIVE_REGION_ROLES = new Set([
"alert",
"log",
"marquee",
"status",
"timer",
]);
// https://www.w3.org/TR/wai-aria/#window_roles
const WINDOW_ROLES = new Set(["alertdialog", "dialog"]);
const ALL_ROLES = new Set([
...DOCUMENT_STRUCTURE_ROLES,
...WIDGET_ROLES,
...COMPOSITE_WIDGET_ROLES,
...LANDMARK_ROLES,
...LIVE_REGION_ROLES,
...WINDOW_ROLES,
]);
const ELEMENTS_DISALLOWING_PRESENTATION_OR_NONE_ROLE = new Set([
"a",
"abbr",
"applet",
"area",
"audio",
"b",
"bdi",
"bdo",
"blockquote",
"br",
"button",
"caption",
"cite",
"code",
"col",
"colgroup",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"dir",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"form",
"hr",
"i",
"iframe",
"input",
"ins",
"kbd",
"label",
"legend",
"main",
"map",
"mark",
"menu",
"menuitem",
"meter",
"noembed",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"param",
"pre",
"progress",
"q",
"rb",
"rp",
"rt",
"rtc",
"ruby",
"s",
"samp",
"select",
"small",
"source",
"strong",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"tr",
"track",
"tt",
"u",
"ul",
"var",
"video",
"wbr",
]);
/** @type {RuleModule<[]>} */
module.exports = {
meta: {
type: "code",
docs: {
description: "Disallows use of invalid role.",
category: RULE_CATEGORY.ACCESSIBILITY,
recommended: false,
url: getRuleUrl("no-invalid-role"),
},
fixable: null,
schema: [],
messages: {
[MESSAGE_IDS.INVALID]: "Unexpected use of invalid role '{{role}}'",
[MESSAGE_IDS.INVALID_PRESENTATION]:
"Unexpected use of presentation role on <{{element}}>",
},
},
create(context) {
return createVisitors(context, {
Tag(node) {
const role = findAttr(node, "role");
if (!role) {
return;
}
/** Allow template expression. ex: html`<div role=${role}></div>` */
if (
role.value &&
role.value.parts.some((part) => part.type === NODE_TYPES.Template)
) {
return;
}
const roleValue = (
(role.value && role.value.value) ||
""
).toLowerCase();
if (
(roleValue === "presentation" || roleValue === "none") &&
ELEMENTS_DISALLOWING_PRESENTATION_OR_NONE_ROLE.has(
node.name.toLowerCase()
)
) {
context.report({
node: role,
messageId: MESSAGE_IDS.INVALID_PRESENTATION,
data: {
element: node.name,
},
});
}
if (!ALL_ROLES.has(roleValue)) {
context.report({
node: role,
messageId: MESSAGE_IDS.INVALID,
data: {
role: roleValue,
},
});
}
},
});
},
};