@flint.fyi/jsx 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +3235 -0
- package/package.json +9 -8
- package/lib/index.d.ts +0 -2
- package/lib/index.js +0 -2
- package/lib/plugin.js +0 -104
- package/lib/rules/accessKeys.d.ts +0 -9
- package/lib/rules/accessKeys.js +0 -30
- package/lib/rules/altTexts.d.ts +0 -9
- package/lib/rules/altTexts.js +0 -76
- package/lib/rules/anchorAmbiguousText.d.ts +0 -9
- package/lib/rules/anchorAmbiguousText.js +0 -55
- package/lib/rules/anchorContent.d.ts +0 -9
- package/lib/rules/anchorContent.js +0 -55
- package/lib/rules/anchorValidity.d.ts +0 -9
- package/lib/rules/anchorValidity.js +0 -77
- package/lib/rules/ariaActiveDescendantTabIndex.d.ts +0 -9
- package/lib/rules/ariaActiveDescendantTabIndex.js +0 -51
- package/lib/rules/ariaHiddenFocusables.d.ts +0 -9
- package/lib/rules/ariaHiddenFocusables.js +0 -66
- package/lib/rules/ariaPropTypes.d.ts +0 -9
- package/lib/rules/ariaPropTypes.js +0 -222
- package/lib/rules/ariaProps.d.ts +0 -9
- package/lib/rules/ariaProps.js +0 -90
- package/lib/rules/ariaRoleValidity.d.ts +0 -9
- package/lib/rules/ariaRoleValidity.js +0 -111
- package/lib/rules/ariaUnsupportedElements.d.ts +0 -9
- package/lib/rules/ariaUnsupportedElements.js +0 -86
- package/lib/rules/autoFocusProps.d.ts +0 -9
- package/lib/rules/autoFocusProps.js +0 -42
- package/lib/rules/autocomplete.d.ts +0 -9
- package/lib/rules/autocomplete.js +0 -120
- package/lib/rules/booleanValues.d.ts +0 -9
- package/lib/rules/booleanValues.js +0 -31
- package/lib/rules/bracedStatements.d.ts +0 -9
- package/lib/rules/bracedStatements.js +0 -31
- package/lib/rules/buttonTypes.d.ts +0 -9
- package/lib/rules/buttonTypes.js +0 -68
- package/lib/rules/childrenProps.d.ts +0 -9
- package/lib/rules/childrenProps.js +0 -37
- package/lib/rules/clickEventKeyEvents.d.ts +0 -9
- package/lib/rules/clickEventKeyEvents.js +0 -54
- package/lib/rules/commentTextNodes.d.ts +0 -9
- package/lib/rules/commentTextNodes.js +0 -30
- package/lib/rules/distractingElements.d.ts +0 -9
- package/lib/rules/distractingElements.js +0 -38
- package/lib/rules/elementChildrenValidity.d.ts +0 -9
- package/lib/rules/elementChildrenValidity.js +0 -52
- package/lib/rules/headingContents.d.ts +0 -9
- package/lib/rules/headingContents.js +0 -52
- package/lib/rules/htmlLangs.d.ts +0 -9
- package/lib/rules/htmlLangs.js +0 -30
- package/lib/rules/iframeTitles.d.ts +0 -9
- package/lib/rules/iframeTitles.js +0 -61
- package/lib/rules/interactiveElementRoles.d.ts +0 -9
- package/lib/rules/interactiveElementRoles.js +0 -91
- package/lib/rules/interactiveElementsFocusable.d.ts +0 -9
- package/lib/rules/interactiveElementsFocusable.js +0 -170
- package/lib/rules/labelAssociatedControls.d.ts +0 -9
- package/lib/rules/labelAssociatedControls.js +0 -68
- package/lib/rules/langValidity.d.ts +0 -9
- package/lib/rules/langValidity.js +0 -41
- package/lib/rules/mediaCaptions.d.ts +0 -9
- package/lib/rules/mediaCaptions.js +0 -50
- package/lib/rules/mouseEventKeyEvents.d.ts +0 -9
- package/lib/rules/mouseEventKeyEvents.js +0 -47
- package/lib/rules/nonInteractiveElementInteractions.d.ts +0 -9
- package/lib/rules/nonInteractiveElementInteractions.js +0 -93
- package/lib/rules/nonInteractiveElementRoles.d.ts +0 -9
- package/lib/rules/nonInteractiveElementRoles.js +0 -116
- package/lib/rules/nonInteractiveElementTabIndexes.d.ts +0 -9
- package/lib/rules/nonInteractiveElementTabIndexes.js +0 -107
- package/lib/rules/propDuplicates.d.ts +0 -9
- package/lib/rules/propDuplicates.js +0 -37
- package/lib/rules/roleRedundancies.d.ts +0 -9
- package/lib/rules/roleRedundancies.js +0 -74
- package/lib/rules/roleRequiredAriaProps.d.ts +0 -9
- package/lib/rules/roleRequiredAriaProps.js +0 -76
- package/lib/rules/roleSupportedAriaProps.d.ts +0 -9
- package/lib/rules/roleSupportedAriaProps.js +0 -417
- package/lib/rules/roleTags.d.ts +0 -9
- package/lib/rules/roleTags.js +0 -70
- package/lib/rules/ruleCreator.d.ts +0 -2
- package/lib/rules/ruleCreator.js +0 -13
- package/lib/rules/scopeProps.d.ts +0 -9
- package/lib/rules/scopeProps.js +0 -39
- package/lib/rules/staticElementInteractions.d.ts +0 -9
- package/lib/rules/staticElementInteractions.js +0 -58
- package/lib/rules/svgTitles.d.ts +0 -9
- package/lib/rules/svgTitles.js +0 -57
- package/lib/rules/tabIndexPositiveValues.d.ts +0 -9
- package/lib/rules/tabIndexPositiveValues.js +0 -43
- package/lib/rules/unescapedEntities.d.ts +0 -9
- package/lib/rules/unescapedEntities.js +0 -68
- package/lib/rules/unnecessaryFragments.d.ts +0 -9
- package/lib/rules/unnecessaryFragments.js +0 -49
- /package/{lib/plugin.d.ts → dist/index.d.mts} +0 -0
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports <iframe> elements without a title prop.";
|
|
3
|
-
readonly id: "iframeTitles";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "missingTitle", undefined>;
|
|
9
|
-
export default _default;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
-
import { SyntaxKind } from "typescript";
|
|
3
|
-
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
-
//#region src/rules/iframeTitles.ts
|
|
5
|
-
var iframeTitles_default = ruleCreator.createRule(typescriptLanguage, {
|
|
6
|
-
about: {
|
|
7
|
-
description: "Reports <iframe> elements without a title prop.",
|
|
8
|
-
id: "iframeTitles",
|
|
9
|
-
presets: ["logical", "logicalStrict"]
|
|
10
|
-
},
|
|
11
|
-
messages: { missingTitle: {
|
|
12
|
-
primary: "This <iframe> element is missing a `title` prop.",
|
|
13
|
-
secondary: [
|
|
14
|
-
"The title attribute provides a label for the iframe that describes its content to screen reader users.",
|
|
15
|
-
"Without it, users may have difficulty understanding the purpose of the iframe.",
|
|
16
|
-
"This is required for WCAG 2.4.1 and 4.1.2 compliance."
|
|
17
|
-
],
|
|
18
|
-
suggestions: ["Add a descriptive title prop (e.g., title=\"Embedded content\")", "Ensure the title clearly describes the iframe's content"]
|
|
19
|
-
} },
|
|
20
|
-
setup(context) {
|
|
21
|
-
function checkIframe({ attributes, tagName }, { sourceFile }) {
|
|
22
|
-
if (tagName.kind !== SyntaxKind.Identifier || tagName.text.toLowerCase() !== "iframe") return;
|
|
23
|
-
const titleAttribute = attributes.properties.find((property) => {
|
|
24
|
-
return property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "title";
|
|
25
|
-
});
|
|
26
|
-
if (titleAttribute?.kind !== SyntaxKind.JsxAttribute) {
|
|
27
|
-
context.report({
|
|
28
|
-
message: "missingTitle",
|
|
29
|
-
range: getTSNodeRange(tagName, sourceFile)
|
|
30
|
-
});
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (!titleAttribute.initializer) {
|
|
34
|
-
context.report({
|
|
35
|
-
message: "missingTitle",
|
|
36
|
-
range: getTSNodeRange(tagName, sourceFile)
|
|
37
|
-
});
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (titleAttribute.initializer.kind === SyntaxKind.StringLiteral) {
|
|
41
|
-
if (titleAttribute.initializer.text === "") context.report({
|
|
42
|
-
message: "missingTitle",
|
|
43
|
-
range: getTSNodeRange(tagName, sourceFile)
|
|
44
|
-
});
|
|
45
|
-
} else if (titleAttribute.initializer.kind === SyntaxKind.JsxExpression) {
|
|
46
|
-
const { expression } = titleAttribute.initializer;
|
|
47
|
-
if (!expression) return;
|
|
48
|
-
if (expression.kind === SyntaxKind.StringLiteral && expression.text === "" || expression.kind === SyntaxKind.NoSubstitutionTemplateLiteral && expression.text === "" || expression.kind === SyntaxKind.Identifier && expression.text === "undefined") context.report({
|
|
49
|
-
message: "missingTitle",
|
|
50
|
-
range: getTSNodeRange(tagName, sourceFile)
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return { visitors: {
|
|
55
|
-
JsxOpeningElement: checkIframe,
|
|
56
|
-
JsxSelfClosingElement: checkIframe
|
|
57
|
-
} };
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
//#endregion
|
|
61
|
-
export { iframeTitles_default as default };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports interactive elements with non-interactive ARIA roles.";
|
|
3
|
-
readonly id: "interactiveElementRoles";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "invalidRole", undefined>;
|
|
9
|
-
export default _default;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
-
import { SyntaxKind } from "typescript";
|
|
3
|
-
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
-
//#region src/rules/interactiveElementRoles.ts
|
|
5
|
-
const interactiveElements = /* @__PURE__ */ new Set([
|
|
6
|
-
"a",
|
|
7
|
-
"audio",
|
|
8
|
-
"button",
|
|
9
|
-
"details",
|
|
10
|
-
"input",
|
|
11
|
-
"select",
|
|
12
|
-
"summary",
|
|
13
|
-
"textarea",
|
|
14
|
-
"video"
|
|
15
|
-
]);
|
|
16
|
-
const nonInteractiveRoles = /* @__PURE__ */ new Set([
|
|
17
|
-
"article",
|
|
18
|
-
"banner",
|
|
19
|
-
"complementary",
|
|
20
|
-
"contentinfo",
|
|
21
|
-
"definition",
|
|
22
|
-
"directory",
|
|
23
|
-
"document",
|
|
24
|
-
"feed",
|
|
25
|
-
"figure",
|
|
26
|
-
"form",
|
|
27
|
-
"group",
|
|
28
|
-
"heading",
|
|
29
|
-
"img",
|
|
30
|
-
"list",
|
|
31
|
-
"listitem",
|
|
32
|
-
"log",
|
|
33
|
-
"main",
|
|
34
|
-
"marquee",
|
|
35
|
-
"math",
|
|
36
|
-
"meter",
|
|
37
|
-
"navigation",
|
|
38
|
-
"none",
|
|
39
|
-
"note",
|
|
40
|
-
"presentation",
|
|
41
|
-
"region",
|
|
42
|
-
"rowgroup",
|
|
43
|
-
"search",
|
|
44
|
-
"separator",
|
|
45
|
-
"status",
|
|
46
|
-
"table",
|
|
47
|
-
"tabpanel",
|
|
48
|
-
"term",
|
|
49
|
-
"timer",
|
|
50
|
-
"tooltip"
|
|
51
|
-
]);
|
|
52
|
-
var interactiveElementRoles_default = ruleCreator.createRule(typescriptLanguage, {
|
|
53
|
-
about: {
|
|
54
|
-
description: "Reports interactive elements with non-interactive ARIA roles.",
|
|
55
|
-
id: "interactiveElementRoles",
|
|
56
|
-
presets: ["logical", "logicalStrict"]
|
|
57
|
-
},
|
|
58
|
-
messages: { invalidRole: {
|
|
59
|
-
primary: "Interactive element <{{ element }}> should not have the non-interactive role `'{{ role }}'`.",
|
|
60
|
-
secondary: [
|
|
61
|
-
"Interactive elements should not be converted to non-interactive elements using ARIA roles.",
|
|
62
|
-
"This removes expected browser interactions from the element.",
|
|
63
|
-
"This is required for WCAG 4.1.2 compliance."
|
|
64
|
-
],
|
|
65
|
-
suggestions: ["Remove the role attribute", "Use a native non-interactive element instead"]
|
|
66
|
-
} },
|
|
67
|
-
setup(context) {
|
|
68
|
-
function checkElement(element, { sourceFile }) {
|
|
69
|
-
if (element.tagName.kind !== SyntaxKind.Identifier) return;
|
|
70
|
-
const elementName = element.tagName.text.toLowerCase();
|
|
71
|
-
if (!interactiveElements.has(elementName)) return;
|
|
72
|
-
const roleProperty = element.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
|
|
73
|
-
if (roleProperty?.kind !== SyntaxKind.JsxAttribute || roleProperty.initializer?.kind !== SyntaxKind.StringLiteral) return;
|
|
74
|
-
const role = roleProperty.initializer.text;
|
|
75
|
-
if (nonInteractiveRoles.has(role)) context.report({
|
|
76
|
-
data: {
|
|
77
|
-
element: elementName,
|
|
78
|
-
role
|
|
79
|
-
},
|
|
80
|
-
message: "invalidRole",
|
|
81
|
-
range: getTSNodeRange(roleProperty, sourceFile)
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
return { visitors: {
|
|
85
|
-
JsxOpeningElement: checkElement,
|
|
86
|
-
JsxSelfClosingElement: checkElement
|
|
87
|
-
} };
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
//#endregion
|
|
91
|
-
export { interactiveElementRoles_default as default };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports interactive elements that are not focusable via keyboard.";
|
|
3
|
-
readonly id: "interactiveElementsFocusable";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "notFocusable", undefined>;
|
|
9
|
-
export default _default;
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
-
import { SyntaxKind } from "typescript";
|
|
3
|
-
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
-
//#region src/rules/interactiveElementsFocusable.ts
|
|
5
|
-
const interactiveHandlers = [
|
|
6
|
-
"onClick",
|
|
7
|
-
"onKeyDown",
|
|
8
|
-
"onKeyPress",
|
|
9
|
-
"onKeyUp",
|
|
10
|
-
"onMouseDown",
|
|
11
|
-
"onMouseUp"
|
|
12
|
-
];
|
|
13
|
-
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
14
|
-
"button",
|
|
15
|
-
"checkbox",
|
|
16
|
-
"columnheader",
|
|
17
|
-
"combobox",
|
|
18
|
-
"grid",
|
|
19
|
-
"gridcell",
|
|
20
|
-
"link",
|
|
21
|
-
"listbox",
|
|
22
|
-
"menu",
|
|
23
|
-
"menubar",
|
|
24
|
-
"menuitem",
|
|
25
|
-
"menuitemcheckbox",
|
|
26
|
-
"menuitemradio",
|
|
27
|
-
"option",
|
|
28
|
-
"progressbar",
|
|
29
|
-
"radio",
|
|
30
|
-
"radiogroup",
|
|
31
|
-
"row",
|
|
32
|
-
"rowheader",
|
|
33
|
-
"searchbox",
|
|
34
|
-
"slider",
|
|
35
|
-
"spinbutton",
|
|
36
|
-
"switch",
|
|
37
|
-
"tab",
|
|
38
|
-
"tablist",
|
|
39
|
-
"textbox",
|
|
40
|
-
"toolbar",
|
|
41
|
-
"tree",
|
|
42
|
-
"treegrid",
|
|
43
|
-
"treeitem"
|
|
44
|
-
]);
|
|
45
|
-
const inherentlyFocusableElements = /* @__PURE__ */ new Set([
|
|
46
|
-
"a",
|
|
47
|
-
"audio",
|
|
48
|
-
"button",
|
|
49
|
-
"input",
|
|
50
|
-
"select",
|
|
51
|
-
"textarea",
|
|
52
|
-
"video"
|
|
53
|
-
]);
|
|
54
|
-
const nonInteractiveElements = /* @__PURE__ */ new Set([
|
|
55
|
-
"article",
|
|
56
|
-
"aside",
|
|
57
|
-
"body",
|
|
58
|
-
"br",
|
|
59
|
-
"details",
|
|
60
|
-
"div",
|
|
61
|
-
"footer",
|
|
62
|
-
"h1",
|
|
63
|
-
"h2",
|
|
64
|
-
"h3",
|
|
65
|
-
"h4",
|
|
66
|
-
"h5",
|
|
67
|
-
"h6",
|
|
68
|
-
"header",
|
|
69
|
-
"hr",
|
|
70
|
-
"img",
|
|
71
|
-
"li",
|
|
72
|
-
"main",
|
|
73
|
-
"nav",
|
|
74
|
-
"ol",
|
|
75
|
-
"p",
|
|
76
|
-
"section",
|
|
77
|
-
"span",
|
|
78
|
-
"table",
|
|
79
|
-
"td",
|
|
80
|
-
"ul"
|
|
81
|
-
]);
|
|
82
|
-
const nonInteractiveRoles = /* @__PURE__ */ new Set([
|
|
83
|
-
"article",
|
|
84
|
-
"banner",
|
|
85
|
-
"complementary",
|
|
86
|
-
"img",
|
|
87
|
-
"listitem",
|
|
88
|
-
"main",
|
|
89
|
-
"navigation",
|
|
90
|
-
"region",
|
|
91
|
-
"tooltip"
|
|
92
|
-
]);
|
|
93
|
-
var interactiveElementsFocusable_default = ruleCreator.createRule(typescriptLanguage, {
|
|
94
|
-
about: {
|
|
95
|
-
description: "Reports interactive elements that are not focusable via keyboard.",
|
|
96
|
-
id: "interactiveElementsFocusable",
|
|
97
|
-
presets: ["logical", "logicalStrict"]
|
|
98
|
-
},
|
|
99
|
-
messages: { notFocusable: {
|
|
100
|
-
primary: "The '{{ role }}' role makes this element interactive, so it should also be focusable.",
|
|
101
|
-
secondary: [
|
|
102
|
-
"Interactive elements must be reachable via keyboard navigation.",
|
|
103
|
-
"Add a tabIndex attribute or use an inherently focusable element.",
|
|
104
|
-
"This is required for WCAG 2.1.1 compliance."
|
|
105
|
-
],
|
|
106
|
-
suggestions: [
|
|
107
|
-
"Add tabIndex={0} to make the element tabbable",
|
|
108
|
-
"Add tabIndex={-1} to make the element programmatically focusable",
|
|
109
|
-
"Use a native interactive element like <button> or <a>"
|
|
110
|
-
]
|
|
111
|
-
} },
|
|
112
|
-
setup(context) {
|
|
113
|
-
function getTabIndexValue(node) {
|
|
114
|
-
const tabIndex = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "tabIndex");
|
|
115
|
-
if (!tabIndex?.initializer) return;
|
|
116
|
-
if (tabIndex.initializer.kind === SyntaxKind.JsxExpression) {
|
|
117
|
-
const expression = tabIndex.initializer.expression;
|
|
118
|
-
if (expression?.kind === SyntaxKind.NumericLiteral) return Number(expression.text);
|
|
119
|
-
}
|
|
120
|
-
if (tabIndex.initializer.kind === SyntaxKind.StringLiteral) return Number(tabIndex.initializer.text);
|
|
121
|
-
}
|
|
122
|
-
function getRoleValue(node) {
|
|
123
|
-
const role = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
|
|
124
|
-
if (role?.kind === SyntaxKind.JsxAttribute && role.initializer?.kind === SyntaxKind.StringLiteral) return role.initializer.text;
|
|
125
|
-
}
|
|
126
|
-
function isAriaHidden(node) {
|
|
127
|
-
const ariaHidden = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "aria-hidden");
|
|
128
|
-
if (ariaHidden?.kind !== SyntaxKind.JsxAttribute || !ariaHidden.initializer) return false;
|
|
129
|
-
if (ariaHidden.initializer.kind === SyntaxKind.JsxExpression) return ariaHidden.initializer.expression?.kind === SyntaxKind.TrueKeyword;
|
|
130
|
-
if (ariaHidden.initializer.kind === SyntaxKind.StringLiteral) return ariaHidden.initializer.text === "true";
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
function hasInteractiveHandler(node) {
|
|
134
|
-
return node.attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && interactiveHandlers.includes(property.name.text));
|
|
135
|
-
}
|
|
136
|
-
function isDisabled(node) {
|
|
137
|
-
const disabledProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "disabled");
|
|
138
|
-
if (disabledProperty?.kind !== SyntaxKind.JsxAttribute) return false;
|
|
139
|
-
if (!disabledProperty.initializer) return true;
|
|
140
|
-
if (disabledProperty.initializer.kind === SyntaxKind.JsxExpression) return disabledProperty.initializer.expression?.kind === SyntaxKind.TrueKeyword;
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
function checkElement(node, { sourceFile }) {
|
|
144
|
-
if (node.tagName.kind !== SyntaxKind.Identifier) return;
|
|
145
|
-
const elementName = node.tagName.text.toLowerCase();
|
|
146
|
-
if (elementName !== node.tagName.text || !hasInteractiveHandler(node) || isAriaHidden(node) || isDisabled(node)) return;
|
|
147
|
-
const role = getRoleValue(node);
|
|
148
|
-
if (role === "presentation" || role === "none") return;
|
|
149
|
-
if (!(role !== void 0 && interactiveRoles.has(role) || role === void 0 && !nonInteractiveElements.has(elementName))) return;
|
|
150
|
-
if (role && nonInteractiveRoles.has(role)) return;
|
|
151
|
-
const hasInherentFocus = inherentlyFocusableElements.has(elementName);
|
|
152
|
-
const hasFocusableTabIndex = getTabIndexValue(node) !== void 0;
|
|
153
|
-
const roleProperty = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "role");
|
|
154
|
-
if (!hasInherentFocus && !hasFocusableTabIndex) {
|
|
155
|
-
const displayRole = role ?? elementName;
|
|
156
|
-
context.report({
|
|
157
|
-
data: { role: displayRole },
|
|
158
|
-
message: "notFocusable",
|
|
159
|
-
range: getTSNodeRange(roleProperty?.kind === SyntaxKind.JsxAttribute ? roleProperty : node.tagName, sourceFile)
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return { visitors: {
|
|
164
|
-
JsxOpeningElement: checkElement,
|
|
165
|
-
JsxSelfClosingElement: checkElement
|
|
166
|
-
} };
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
//#endregion
|
|
170
|
-
export { interactiveElementsFocusable_default as default };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports <label> elements without an associated control element.";
|
|
3
|
-
readonly id: "labelAssociatedControls";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "missingAssociatedControl", undefined>;
|
|
9
|
-
export default _default;
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
-
import { SyntaxKind } from "typescript";
|
|
3
|
-
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
-
//#region src/rules/labelAssociatedControls.ts
|
|
5
|
-
const controlElements = /* @__PURE__ */ new Set([
|
|
6
|
-
"input",
|
|
7
|
-
"meter",
|
|
8
|
-
"output",
|
|
9
|
-
"progress",
|
|
10
|
-
"select",
|
|
11
|
-
"textarea"
|
|
12
|
-
]);
|
|
13
|
-
var labelAssociatedControls_default = ruleCreator.createRule(typescriptLanguage, {
|
|
14
|
-
about: {
|
|
15
|
-
description: "Reports <label> elements without an associated control element.",
|
|
16
|
-
id: "labelAssociatedControls",
|
|
17
|
-
presets: ["logical", "logicalStrict"]
|
|
18
|
-
},
|
|
19
|
-
messages: { missingAssociatedControl: {
|
|
20
|
-
primary: "This <label> element is missing an associated control element.",
|
|
21
|
-
secondary: [
|
|
22
|
-
"Labels must be associated with a control element to be accessible to screen readers.",
|
|
23
|
-
"Use the htmlFor prop to reference a control by id, or nest the control inside the label.",
|
|
24
|
-
"This is required for WCAG 1.3.1, 2.4.6, and 4.1.2 compliance."
|
|
25
|
-
],
|
|
26
|
-
suggestions: ["Add an htmlFor prop that references a control element by id", "Nest a control element (input, select, textarea) inside the label"]
|
|
27
|
-
} },
|
|
28
|
-
setup(context) {
|
|
29
|
-
function hasHtmlForAttribute(attributes) {
|
|
30
|
-
return attributes.properties.some((property) => {
|
|
31
|
-
if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier || property.name.text !== "htmlFor" || !property.initializer) return false;
|
|
32
|
-
if (property.initializer.kind === SyntaxKind.StringLiteral) return property.initializer.text !== "";
|
|
33
|
-
if (property.initializer.kind === SyntaxKind.JsxExpression) {
|
|
34
|
-
const { expression } = property.initializer;
|
|
35
|
-
if (!expression) return false;
|
|
36
|
-
if (expression.kind === SyntaxKind.StringLiteral && expression.text === "" || expression.kind === SyntaxKind.NoSubstitutionTemplateLiteral && expression.text === "" || expression.kind === SyntaxKind.Identifier && expression.text === "undefined" || expression.kind === SyntaxKind.NullKeyword) return false;
|
|
37
|
-
}
|
|
38
|
-
return true;
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function hasNestedControl(children) {
|
|
42
|
-
return children.some((child) => {
|
|
43
|
-
if (child.kind === SyntaxKind.JsxElement) {
|
|
44
|
-
const { tagName } = child.openingElement;
|
|
45
|
-
return tagName.kind === SyntaxKind.Identifier && controlElements.has(tagName.text.toLowerCase()) || hasNestedControl(child.children);
|
|
46
|
-
}
|
|
47
|
-
if (child.kind === SyntaxKind.JsxSelfClosingElement) return child.tagName.kind === SyntaxKind.Identifier && controlElements.has(child.tagName.text.toLowerCase());
|
|
48
|
-
return false;
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
function checkLabel(node, { sourceFile }) {
|
|
52
|
-
if (node.kind === SyntaxKind.JsxElement && hasNestedControl(node.children)) return;
|
|
53
|
-
const tagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
|
|
54
|
-
if (tagName.kind !== SyntaxKind.Identifier || tagName.text.toLowerCase() !== "label") return;
|
|
55
|
-
if (hasHtmlForAttribute(node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes)) return;
|
|
56
|
-
context.report({
|
|
57
|
-
message: "missingAssociatedControl",
|
|
58
|
-
range: getTSNodeRange(tagName, sourceFile)
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
return { visitors: {
|
|
62
|
-
JsxElement: checkLabel,
|
|
63
|
-
JsxSelfClosingElement: checkLabel
|
|
64
|
-
} };
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
//#endregion
|
|
68
|
-
export { labelAssociatedControls_default as default };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports invalid lang attribute values.";
|
|
3
|
-
readonly id: "langValidity";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "invalidLang", undefined>;
|
|
9
|
-
export default _default;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
-
import { SyntaxKind } from "typescript";
|
|
3
|
-
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
-
import languageTags from "language-tags";
|
|
5
|
-
//#region src/rules/langValidity.ts
|
|
6
|
-
var langValidity_default = ruleCreator.createRule(typescriptLanguage, {
|
|
7
|
-
about: {
|
|
8
|
-
description: "Reports invalid lang attribute values.",
|
|
9
|
-
id: "langValidity",
|
|
10
|
-
presets: ["logical", "logicalStrict"]
|
|
11
|
-
},
|
|
12
|
-
messages: { invalidLang: {
|
|
13
|
-
primary: "The lang attribute value '{{ value }}' is not a valid BCP 47 language tag.",
|
|
14
|
-
secondary: [
|
|
15
|
-
"The lang attribute must contain a valid BCP 47 language tag.",
|
|
16
|
-
"Valid examples include 'en', 'en-US', 'zh-Hans', or 'fr-CA'.",
|
|
17
|
-
"This is required for WCAG 3.1.2 compliance."
|
|
18
|
-
],
|
|
19
|
-
suggestions: ["Use a valid BCP 47 language tag like 'en' or 'en-US'", "Check the language code spelling and format"]
|
|
20
|
-
} },
|
|
21
|
-
setup(context) {
|
|
22
|
-
function checkElement(node, { sourceFile }) {
|
|
23
|
-
const langAttribute = node.attributes.properties.find((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text === "lang");
|
|
24
|
-
if (!langAttribute?.initializer) return;
|
|
25
|
-
if (langAttribute.initializer.kind === SyntaxKind.StringLiteral) {
|
|
26
|
-
const langValue = langAttribute.initializer.text;
|
|
27
|
-
if (!languageTags.check(langValue)) context.report({
|
|
28
|
-
data: { value: langValue || "(empty)" },
|
|
29
|
-
message: "invalidLang",
|
|
30
|
-
range: getTSNodeRange(langAttribute.initializer, sourceFile)
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return { visitors: {
|
|
35
|
-
JsxOpeningElement: checkElement,
|
|
36
|
-
JsxSelfClosingElement: checkElement
|
|
37
|
-
} };
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
//#endregion
|
|
41
|
-
export { langValidity_default as default };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports media elements without captions.";
|
|
3
|
-
readonly id: "mediaCaptions";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "missingCaptions", undefined>;
|
|
9
|
-
export default _default;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
-
import { SyntaxKind } from "typescript";
|
|
3
|
-
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
-
//#region src/rules/mediaCaptions.ts
|
|
5
|
-
var mediaCaptions_default = ruleCreator.createRule(typescriptLanguage, {
|
|
6
|
-
about: {
|
|
7
|
-
description: "Reports media elements without captions.",
|
|
8
|
-
id: "mediaCaptions",
|
|
9
|
-
presets: ["logical", "logicalStrict"]
|
|
10
|
-
},
|
|
11
|
-
messages: { missingCaptions: {
|
|
12
|
-
primary: "This media element is missing <track> element captions.",
|
|
13
|
-
secondary: [
|
|
14
|
-
"Captions are essential for deaf users to follow along with media content.",
|
|
15
|
-
"The <track> element with kind='captions' provides this accessibility feature.",
|
|
16
|
-
"This is required for WCAG 1.2.2 and 1.2.3 compliance."
|
|
17
|
-
],
|
|
18
|
-
suggestions: ["Add a <track kind=\"captions\"> element as a child", "Add the muted attribute if the media has no audio"]
|
|
19
|
-
} },
|
|
20
|
-
setup(context) {
|
|
21
|
-
function checkMediaElement(node, { sourceFile }) {
|
|
22
|
-
const tagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
|
|
23
|
-
if (tagName.kind !== SyntaxKind.Identifier) return;
|
|
24
|
-
const elementName = tagName.text.toLowerCase();
|
|
25
|
-
if (elementName !== "audio" && elementName !== "video") return;
|
|
26
|
-
if ((node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes).properties.some((properties) => properties.kind === SyntaxKind.JsxAttribute && properties.name.kind === SyntaxKind.Identifier && properties.name.text === "muted")) return;
|
|
27
|
-
if (node.kind === SyntaxKind.JsxElement && node.children.some(isCaptionsTrack)) return;
|
|
28
|
-
context.report({
|
|
29
|
-
message: "missingCaptions",
|
|
30
|
-
range: getTSNodeRange(tagName, sourceFile)
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
return { visitors: {
|
|
34
|
-
JsxElement: checkMediaElement,
|
|
35
|
-
JsxSelfClosingElement: checkMediaElement
|
|
36
|
-
} };
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
function isCaptionsTrack(node) {
|
|
40
|
-
if (node.kind !== SyntaxKind.JsxElement && node.kind !== SyntaxKind.JsxSelfClosingElement) return false;
|
|
41
|
-
const childTagName = node.kind === SyntaxKind.JsxElement ? node.openingElement.tagName : node.tagName;
|
|
42
|
-
if (childTagName.kind !== SyntaxKind.Identifier || childTagName.text !== "track") return false;
|
|
43
|
-
return (node.kind === SyntaxKind.JsxElement ? node.openingElement.attributes : node.attributes).properties.some((property) => {
|
|
44
|
-
if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier || property.name.text !== "kind") return false;
|
|
45
|
-
if (property.initializer?.kind === SyntaxKind.StringLiteral) return property.initializer.text === "captions";
|
|
46
|
-
return false;
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
//#endregion
|
|
50
|
-
export { mediaCaptions_default as default };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports mouse events without corresponding keyboard events.";
|
|
3
|
-
readonly id: "mouseEventKeyEvents";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "missingKeyEvent", undefined>;
|
|
9
|
-
export default _default;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { ruleCreator } from "./ruleCreator.js";
|
|
2
|
-
import { SyntaxKind } from "typescript";
|
|
3
|
-
import { getTSNodeRange, typescriptLanguage } from "@flint.fyi/typescript-language";
|
|
4
|
-
//#region src/rules/mouseEventKeyEvents.ts
|
|
5
|
-
const mouseNamesToKeyboardNames = /* @__PURE__ */ new Map([["onMouseOut", "onBlur"], ["onMouseOver", "onFocus"]]);
|
|
6
|
-
var mouseEventKeyEvents_default = ruleCreator.createRule(typescriptLanguage, {
|
|
7
|
-
about: {
|
|
8
|
-
description: "Reports mouse events without corresponding keyboard events.",
|
|
9
|
-
id: "mouseEventKeyEvents",
|
|
10
|
-
presets: ["logical", "logicalStrict"]
|
|
11
|
-
},
|
|
12
|
-
messages: { missingKeyEvent: {
|
|
13
|
-
primary: "`{{ mouseEvent }}` is missing an accompanying `{{ keyEvent }}` for keyboard accessibility.",
|
|
14
|
-
secondary: [
|
|
15
|
-
"Mouse events should have keyboard equivalents for users who cannot use a mouse.",
|
|
16
|
-
"This ensures that functionality is accessible to all users, such as those with limited hardware or personal disabilities.",
|
|
17
|
-
"Required for WCAG 2.1.1 compliance."
|
|
18
|
-
],
|
|
19
|
-
suggestions: ["Add the corresponding keyboard event handler", "Ensure both mouse and keyboard users can access the functionality"]
|
|
20
|
-
} },
|
|
21
|
-
setup(context) {
|
|
22
|
-
function checkMouseEvents(node, { sourceFile }) {
|
|
23
|
-
const { attributes } = node;
|
|
24
|
-
const presentAttributes = /* @__PURE__ */ new Set();
|
|
25
|
-
for (const property of attributes.properties) if (property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier) presentAttributes.add(property.name.text);
|
|
26
|
-
for (const property of attributes.properties) {
|
|
27
|
-
if (property.kind !== SyntaxKind.JsxAttribute || property.name.kind !== SyntaxKind.Identifier) continue;
|
|
28
|
-
const mouseEvent = property.name.text;
|
|
29
|
-
const keyEvent = mouseNamesToKeyboardNames.get(mouseEvent);
|
|
30
|
-
if (keyEvent && !presentAttributes.has(keyEvent)) context.report({
|
|
31
|
-
data: {
|
|
32
|
-
keyEvent,
|
|
33
|
-
mouseEvent
|
|
34
|
-
},
|
|
35
|
-
message: "missingKeyEvent",
|
|
36
|
-
range: getTSNodeRange(property.name, sourceFile)
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return { visitors: {
|
|
41
|
-
JsxOpeningElement: checkMouseEvents,
|
|
42
|
-
JsxSelfClosingElement: checkMouseEvents
|
|
43
|
-
} };
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
//#endregion
|
|
47
|
-
export { mouseEventKeyEvents_default as default };
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
declare const _default: import("@flint.fyi/core").Rule<{
|
|
2
|
-
readonly description: "Reports non-interactive elements with interactive event handlers.";
|
|
3
|
-
readonly id: "nonInteractiveElementInteractions";
|
|
4
|
-
readonly presets: readonly ["logical", "logicalStrict"];
|
|
5
|
-
} & {
|
|
6
|
-
readonly pluginId: string;
|
|
7
|
-
readonly url: string;
|
|
8
|
-
}, "invalidHandler", undefined>;
|
|
9
|
-
export default _default;
|