@flint.fyi/jsx 0.3.3 → 0.5.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.
Files changed (96) hide show
  1. package/{lib/plugin.d.ts → dist/index.d.mts} +45 -48
  2. package/dist/index.mjs +3235 -0
  3. package/package.json +10 -10
  4. package/lib/index.d.ts +0 -2
  5. package/lib/index.js +0 -2
  6. package/lib/plugin.js +0 -106
  7. package/lib/rules/accessKeys.d.ts +0 -10
  8. package/lib/rules/accessKeys.js +0 -32
  9. package/lib/rules/altTexts.d.ts +0 -10
  10. package/lib/rules/altTexts.js +0 -78
  11. package/lib/rules/anchorAmbiguousText.d.ts +0 -10
  12. package/lib/rules/anchorAmbiguousText.js +0 -57
  13. package/lib/rules/anchorContent.d.ts +0 -10
  14. package/lib/rules/anchorContent.js +0 -57
  15. package/lib/rules/anchorValidity.d.ts +0 -10
  16. package/lib/rules/anchorValidity.js +0 -79
  17. package/lib/rules/ariaActiveDescendantTabIndex.d.ts +0 -10
  18. package/lib/rules/ariaActiveDescendantTabIndex.js +0 -53
  19. package/lib/rules/ariaHiddenFocusables.d.ts +0 -10
  20. package/lib/rules/ariaHiddenFocusables.js +0 -68
  21. package/lib/rules/ariaPropTypes.d.ts +0 -10
  22. package/lib/rules/ariaPropTypes.js +0 -224
  23. package/lib/rules/ariaProps.d.ts +0 -10
  24. package/lib/rules/ariaProps.js +0 -92
  25. package/lib/rules/ariaRoleValidity.d.ts +0 -10
  26. package/lib/rules/ariaRoleValidity.js +0 -113
  27. package/lib/rules/ariaUnsupportedElements.d.ts +0 -10
  28. package/lib/rules/ariaUnsupportedElements.js +0 -88
  29. package/lib/rules/autoFocusProps.d.ts +0 -10
  30. package/lib/rules/autoFocusProps.js +0 -44
  31. package/lib/rules/autocomplete.d.ts +0 -10
  32. package/lib/rules/autocomplete.js +0 -126
  33. package/lib/rules/booleanValues.d.ts +0 -10
  34. package/lib/rules/booleanValues.js +0 -33
  35. package/lib/rules/bracedStatements.d.ts +0 -10
  36. package/lib/rules/bracedStatements.js +0 -33
  37. package/lib/rules/buttonTypes.d.ts +0 -10
  38. package/lib/rules/buttonTypes.js +0 -76
  39. package/lib/rules/childrenProps.d.ts +0 -10
  40. package/lib/rules/childrenProps.js +0 -39
  41. package/lib/rules/clickEventKeyEvents.d.ts +0 -10
  42. package/lib/rules/clickEventKeyEvents.js +0 -56
  43. package/lib/rules/commentTextNodes.d.ts +0 -10
  44. package/lib/rules/commentTextNodes.js +0 -32
  45. package/lib/rules/distractingElements.d.ts +0 -10
  46. package/lib/rules/distractingElements.js +0 -40
  47. package/lib/rules/elementChildrenValidity.d.ts +0 -10
  48. package/lib/rules/elementChildrenValidity.js +0 -54
  49. package/lib/rules/headingContents.d.ts +0 -10
  50. package/lib/rules/headingContents.js +0 -54
  51. package/lib/rules/htmlLangs.d.ts +0 -10
  52. package/lib/rules/htmlLangs.js +0 -32
  53. package/lib/rules/iframeTitles.d.ts +0 -10
  54. package/lib/rules/iframeTitles.js +0 -63
  55. package/lib/rules/interactiveElementRoles.d.ts +0 -10
  56. package/lib/rules/interactiveElementRoles.js +0 -93
  57. package/lib/rules/interactiveElementsFocusable.d.ts +0 -10
  58. package/lib/rules/interactiveElementsFocusable.js +0 -172
  59. package/lib/rules/labelAssociatedControls.d.ts +0 -10
  60. package/lib/rules/labelAssociatedControls.js +0 -70
  61. package/lib/rules/langValidity.d.ts +0 -10
  62. package/lib/rules/langValidity.js +0 -43
  63. package/lib/rules/mediaCaptions.d.ts +0 -10
  64. package/lib/rules/mediaCaptions.js +0 -52
  65. package/lib/rules/mouseEventKeyEvents.d.ts +0 -10
  66. package/lib/rules/mouseEventKeyEvents.js +0 -49
  67. package/lib/rules/nonInteractiveElementInteractions.d.ts +0 -10
  68. package/lib/rules/nonInteractiveElementInteractions.js +0 -95
  69. package/lib/rules/nonInteractiveElementRoles.d.ts +0 -10
  70. package/lib/rules/nonInteractiveElementRoles.js +0 -118
  71. package/lib/rules/nonInteractiveElementTabIndexes.d.ts +0 -10
  72. package/lib/rules/nonInteractiveElementTabIndexes.js +0 -109
  73. package/lib/rules/propDuplicates.d.ts +0 -10
  74. package/lib/rules/propDuplicates.js +0 -39
  75. package/lib/rules/roleRedundancies.d.ts +0 -10
  76. package/lib/rules/roleRedundancies.js +0 -76
  77. package/lib/rules/roleRequiredAriaProps.d.ts +0 -10
  78. package/lib/rules/roleRequiredAriaProps.js +0 -78
  79. package/lib/rules/roleSupportedAriaProps.d.ts +0 -10
  80. package/lib/rules/roleSupportedAriaProps.js +0 -419
  81. package/lib/rules/roleTags.d.ts +0 -10
  82. package/lib/rules/roleTags.js +0 -72
  83. package/lib/rules/ruleCreator.d.ts +0 -3
  84. package/lib/rules/ruleCreator.js +0 -15
  85. package/lib/rules/scopeProps.d.ts +0 -10
  86. package/lib/rules/scopeProps.js +0 -41
  87. package/lib/rules/staticElementInteractions.d.ts +0 -10
  88. package/lib/rules/staticElementInteractions.js +0 -60
  89. package/lib/rules/svgTitles.d.ts +0 -10
  90. package/lib/rules/svgTitles.js +0 -59
  91. package/lib/rules/tabIndexPositiveValues.d.ts +0 -10
  92. package/lib/rules/tabIndexPositiveValues.js +0 -45
  93. package/lib/rules/unescapedEntities.d.ts +0 -10
  94. package/lib/rules/unescapedEntities.js +0 -70
  95. package/lib/rules/unnecessaryFragments.d.ts +0 -10
  96. package/lib/rules/unnecessaryFragments.js +0 -51
@@ -1,10 +0,0 @@
1
- declare const _default: import("@flint.fyi/core").Rule<{
2
- readonly description: "Reports <html> elements without a lang prop.";
3
- readonly id: "htmlLangs";
4
- readonly presets: readonly ["logical", "logicalStrict"];
5
- } & {
6
- readonly pluginId: string;
7
- readonly url: string;
8
- }, "missingLang", undefined>;
9
- export default _default;
10
- //# sourceMappingURL=htmlLangs.d.ts.map
@@ -1,32 +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/htmlLangs.ts
5
- var htmlLangs_default = ruleCreator.createRule(typescriptLanguage, {
6
- about: {
7
- description: "Reports <html> elements without a lang prop.",
8
- id: "htmlLangs",
9
- presets: ["logical", "logicalStrict"]
10
- },
11
- messages: { missingLang: {
12
- primary: "This <html> element is missing a `lang` prop.",
13
- secondary: [
14
- "The lang attribute identifies the language of the document for screen readers and assistive technologies.",
15
- "Without it, screen readers may default to the user's system language, causing confusion.",
16
- "This is required for WCAG 3.1.1 compliance."
17
- ],
18
- suggestions: ["Add a lang prop with a valid language code (e.g., lang=\"en\")", "Use lang=\"en-US\" for more specific localization"]
19
- } },
20
- setup(context) {
21
- return { visitors: { JsxOpeningElement(node, { sourceFile }) {
22
- if (node.tagName.kind === SyntaxKind.Identifier && node.tagName.text === "html" && !node.attributes.properties.some((property) => property.kind === SyntaxKind.JsxAttribute && property.name.kind === SyntaxKind.Identifier && property.name.text.toLowerCase() === "lang")) context.report({
23
- message: "missingLang",
24
- range: getTSNodeRange(node.tagName, sourceFile)
25
- });
26
- } } };
27
- }
28
- });
29
- //#endregion
30
- export { htmlLangs_default as default };
31
-
32
- //# sourceMappingURL=htmlLangs.js.map
@@ -1,10 +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;
10
- //# sourceMappingURL=iframeTitles.d.ts.map
@@ -1,63 +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 };
62
-
63
- //# sourceMappingURL=iframeTitles.js.map
@@ -1,10 +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;
10
- //# sourceMappingURL=interactiveElementRoles.d.ts.map
@@ -1,93 +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 = new Set([
6
- "a",
7
- "audio",
8
- "button",
9
- "details",
10
- "input",
11
- "select",
12
- "summary",
13
- "textarea",
14
- "video"
15
- ]);
16
- const nonInteractiveRoles = 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 };
92
-
93
- //# sourceMappingURL=interactiveElementRoles.js.map
@@ -1,10 +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;
10
- //# sourceMappingURL=interactiveElementsFocusable.d.ts.map
@@ -1,172 +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 = 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 = new Set([
46
- "a",
47
- "audio",
48
- "button",
49
- "input",
50
- "select",
51
- "textarea",
52
- "video"
53
- ]);
54
- const nonInteractiveElements = 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 = 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 };
171
-
172
- //# sourceMappingURL=interactiveElementsFocusable.js.map
@@ -1,10 +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;
10
- //# sourceMappingURL=labelAssociatedControls.d.ts.map
@@ -1,70 +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 = 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 };
69
-
70
- //# sourceMappingURL=labelAssociatedControls.js.map
@@ -1,10 +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;
10
- //# sourceMappingURL=langValidity.d.ts.map
@@ -1,43 +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 };
42
-
43
- //# sourceMappingURL=langValidity.js.map
@@ -1,10 +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;
10
- //# sourceMappingURL=mediaCaptions.d.ts.map
@@ -1,52 +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 };
51
-
52
- //# sourceMappingURL=mediaCaptions.js.map
@@ -1,10 +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;
10
- //# sourceMappingURL=mouseEventKeyEvents.d.ts.map
@@ -1,49 +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 = 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 };
48
-
49
- //# sourceMappingURL=mouseEventKeyEvents.js.map