eslint-plugin-react-a11y 2.1.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/AGENTS.md +110 -0
- package/CHANGELOG.md +37 -0
- package/LICENSE +22 -0
- package/README.md +213 -0
- package/package.json +79 -0
- package/src/index.d.ts +40 -0
- package/src/index.js +279 -0
- package/src/rules/alt-text.d.ts +23 -0
- package/src/rules/alt-text.js +205 -0
- package/src/rules/anchor-ambiguous-text.d.ts +20 -0
- package/src/rules/anchor-ambiguous-text.js +169 -0
- package/src/rules/anchor-has-content.d.ts +20 -0
- package/src/rules/anchor-has-content.js +85 -0
- package/src/rules/anchor-is-valid.d.ts +23 -0
- package/src/rules/anchor-is-valid.js +100 -0
- package/src/rules/aria-activedescendant-has-tabindex.d.ts +15 -0
- package/src/rules/aria-activedescendant-has-tabindex.js +63 -0
- package/src/rules/aria-props.d.ts +15 -0
- package/src/rules/aria-props.js +51 -0
- package/src/rules/aria-role.d.ts +21 -0
- package/src/rules/aria-role.js +72 -0
- package/src/rules/aria-unsupported-elements.d.ts +15 -0
- package/src/rules/aria-unsupported-elements.js +54 -0
- package/src/rules/autocomplete-valid.d.ts +20 -0
- package/src/rules/autocomplete-valid.js +79 -0
- package/src/rules/click-events-have-key-events.d.ts +15 -0
- package/src/rules/click-events-have-key-events.js +60 -0
- package/src/rules/control-has-associated-label.d.ts +24 -0
- package/src/rules/control-has-associated-label.js +178 -0
- package/src/rules/heading-has-content.d.ts +20 -0
- package/src/rules/heading-has-content.js +72 -0
- package/src/rules/html-has-lang.d.ts +15 -0
- package/src/rules/html-has-lang.js +50 -0
- package/src/rules/iframe-has-title.d.ts +15 -0
- package/src/rules/iframe-has-title.js +51 -0
- package/src/rules/img-redundant-alt.d.ts +21 -0
- package/src/rules/img-redundant-alt.js +85 -0
- package/src/rules/interactive-supports-focus.d.ts +20 -0
- package/src/rules/interactive-supports-focus.js +81 -0
- package/src/rules/label-has-associated-control.d.ts +24 -0
- package/src/rules/label-has-associated-control.js +93 -0
- package/src/rules/lang.d.ts +15 -0
- package/src/rules/lang.js +53 -0
- package/src/rules/media-has-caption.d.ts +22 -0
- package/src/rules/media-has-caption.js +84 -0
- package/src/rules/mouse-events-have-key-events.d.ts +17 -0
- package/src/rules/mouse-events-have-key-events.js +62 -0
- package/src/rules/no-access-key.d.ts +15 -0
- package/src/rules/no-access-key.js +43 -0
- package/src/rules/no-aria-hidden-on-focusable.d.ts +15 -0
- package/src/rules/no-aria-hidden-on-focusable.js +67 -0
- package/src/rules/no-autofocus.d.ts +20 -0
- package/src/rules/no-autofocus.js +59 -0
- package/src/rules/no-distracting-elements.d.ts +20 -0
- package/src/rules/no-distracting-elements.js +64 -0
- package/src/rules/no-interactive-element-to-noninteractive-role.d.ts +18 -0
- package/src/rules/no-interactive-element-to-noninteractive-role.js +86 -0
- package/src/rules/no-keyboard-inaccessible-elements.d.ts +24 -0
- package/src/rules/no-keyboard-inaccessible-elements.js +136 -0
- package/src/rules/no-missing-aria-labels.d.ts +24 -0
- package/src/rules/no-missing-aria-labels.js +131 -0
- package/src/rules/no-noninteractive-element-interactions.d.ts +20 -0
- package/src/rules/no-noninteractive-element-interactions.js +78 -0
- package/src/rules/no-noninteractive-element-to-interactive-role.d.ts +18 -0
- package/src/rules/no-noninteractive-element-to-interactive-role.js +95 -0
- package/src/rules/no-noninteractive-tabindex.d.ts +22 -0
- package/src/rules/no-noninteractive-tabindex.js +172 -0
- package/src/rules/no-redundant-roles.d.ts +24 -0
- package/src/rules/no-redundant-roles.js +115 -0
- package/src/rules/no-static-element-interactions.d.ts +20 -0
- package/src/rules/no-static-element-interactions.js +72 -0
- package/src/rules/prefer-tag-over-role.d.ts +15 -0
- package/src/rules/prefer-tag-over-role.js +101 -0
- package/src/rules/role-has-required-aria-props.d.ts +15 -0
- package/src/rules/role-has-required-aria-props.js +65 -0
- package/src/rules/role-supports-aria-props.d.ts +15 -0
- package/src/rules/role-supports-aria-props.js +115 -0
- package/src/rules/scope.d.ts +15 -0
- package/src/rules/scope.js +49 -0
- package/src/rules/tabindex-no-positive.d.ts +15 -0
- package/src/rules/tabindex-no-positive.js +55 -0
- package/src/types/index.d.ts +208 -0
- package/src/types/index.js +7 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.anchorAmbiguousText = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
const DEFAULT_AMBIGUOUS_WORDS = ['click here', 'here', 'link', 'a link', 'learn more'];
|
|
12
|
+
const isJSXAttribute = (attr) => attr.type === 'JSXAttribute';
|
|
13
|
+
/**
|
|
14
|
+
* Extract accessible text from a JSX element
|
|
15
|
+
*/
|
|
16
|
+
function getAccessibleText(element) {
|
|
17
|
+
// Check for aria-label first
|
|
18
|
+
const ariaLabel = element.openingElement.attributes.find((attr) => isJSXAttribute(attr) &&
|
|
19
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
20
|
+
attr.name.name === 'aria-label');
|
|
21
|
+
if (ariaLabel && ariaLabel.type === 'JSXAttribute' && ariaLabel.value && ariaLabel.value.type === 'Literal' && typeof ariaLabel.value.value === 'string') {
|
|
22
|
+
return ariaLabel.value.value;
|
|
23
|
+
}
|
|
24
|
+
// For img elements, use alt text
|
|
25
|
+
if (element.openingElement.name.type === 'JSXIdentifier' && element.openingElement.name.name === 'img') {
|
|
26
|
+
const altAttr = element.openingElement.attributes.find((attr) => isJSXAttribute(attr) &&
|
|
27
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
28
|
+
attr.name.name === 'alt');
|
|
29
|
+
if (altAttr && altAttr.type === 'JSXAttribute' && altAttr.value && altAttr.value.type === 'Literal' && typeof altAttr.value.value === 'string') {
|
|
30
|
+
return altAttr.value.value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Extract text from children
|
|
34
|
+
return extractTextFromChildren(element.children);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extract text content from JSX children
|
|
38
|
+
*/
|
|
39
|
+
function extractTextFromChildren(children) {
|
|
40
|
+
let text = '';
|
|
41
|
+
for (const child of children) {
|
|
42
|
+
if (child.type === 'JSXText') {
|
|
43
|
+
text += child.value;
|
|
44
|
+
}
|
|
45
|
+
else if (child.type === 'JSXElement') {
|
|
46
|
+
// Check if element has aria-hidden
|
|
47
|
+
const hasAriaHidden = child.openingElement.attributes.some((attr) => isJSXAttribute(attr) &&
|
|
48
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
49
|
+
attr.name.name === 'aria-hidden' &&
|
|
50
|
+
attr.value !== null &&
|
|
51
|
+
attr.value.type === 'Literal' &&
|
|
52
|
+
(attr.value.value === true || attr.value.value === 'true'));
|
|
53
|
+
if (!hasAriaHidden) {
|
|
54
|
+
// For img elements in anchor text, use alt
|
|
55
|
+
if (child.openingElement.name.type === 'JSXIdentifier' && child.openingElement.name.name === 'img') {
|
|
56
|
+
const altAttr = child.openingElement.attributes.find((attr) => isJSXAttribute(attr) &&
|
|
57
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
58
|
+
attr.name.name === 'alt');
|
|
59
|
+
if (altAttr && altAttr.type === 'JSXAttribute' && altAttr.value && altAttr.value.type === 'Literal' && typeof altAttr.value.value === 'string') {
|
|
60
|
+
text += altAttr.value.value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
text += extractTextFromChildren(child.children);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (child.type === 'JSXExpressionContainer') {
|
|
69
|
+
// Handle JSX expressions like {variable} or {`template literal`}
|
|
70
|
+
text += `{${extractExpressionText(child.expression)}}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return text.trim();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Extract text from JSX expression
|
|
77
|
+
*/
|
|
78
|
+
function extractExpressionText(expression) {
|
|
79
|
+
if (expression.type === 'JSXEmptyExpression') {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
if (expression.type === 'Identifier') {
|
|
83
|
+
return expression.name;
|
|
84
|
+
}
|
|
85
|
+
else if (expression.type === 'Literal' && typeof expression.value === 'string') {
|
|
86
|
+
return expression.value;
|
|
87
|
+
}
|
|
88
|
+
else if (expression.type === 'TemplateLiteral') {
|
|
89
|
+
let result = '';
|
|
90
|
+
for (let i = 0; i < expression.quasis.length; i++) {
|
|
91
|
+
result += expression.quasis[i].value.raw;
|
|
92
|
+
if (i < expression.expressions.length) {
|
|
93
|
+
result += `\${${extractExpressionText(expression.expressions[i])}}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
else if (expression.type === 'BinaryExpression' && expression.operator === '+') {
|
|
99
|
+
return `${extractExpressionText(expression.left)}+${extractExpressionText(expression.right)}`;
|
|
100
|
+
}
|
|
101
|
+
// For other expression types, return a placeholder
|
|
102
|
+
return '...';
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Normalize text for comparison - only normalize whitespace, preserve punctuation
|
|
106
|
+
*/
|
|
107
|
+
function normalizeText(text) {
|
|
108
|
+
return text
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.trim()
|
|
111
|
+
.replace(/\s+/g, ' '); // Normalize whitespace but preserve punctuation
|
|
112
|
+
}
|
|
113
|
+
exports.anchorAmbiguousText = (0, eslint_devkit_2.createRule)({
|
|
114
|
+
name: 'anchor-ambiguous-text',
|
|
115
|
+
meta: {
|
|
116
|
+
type: 'problem',
|
|
117
|
+
docs: {
|
|
118
|
+
description: 'Enforce that anchor text is not ambiguous',
|
|
119
|
+
},
|
|
120
|
+
messages: {
|
|
121
|
+
ambiguousText: (0, eslint_devkit_1.formatLLMMessage)({
|
|
122
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
123
|
+
issueName: 'Ambiguous Anchor Text',
|
|
124
|
+
description: 'Anchor text "{{text}}" is ambiguous',
|
|
125
|
+
severity: 'MEDIUM',
|
|
126
|
+
fix: 'Use descriptive link text instead of ambiguous phrases',
|
|
127
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-ambiguous-text.md',
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
schema: [
|
|
131
|
+
{
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
words: {
|
|
135
|
+
type: 'array',
|
|
136
|
+
items: { type: 'string' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
additionalProperties: false,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
defaultOptions: [{}],
|
|
144
|
+
create(context, [options = {}]) {
|
|
145
|
+
const { words = DEFAULT_AMBIGUOUS_WORDS } = options ?? {};
|
|
146
|
+
const ambiguousWords = words.map((word) => normalizeText(word));
|
|
147
|
+
return {
|
|
148
|
+
JSXElement(node) {
|
|
149
|
+
if (node.openingElement.name.type !== 'JSXIdentifier' || node.openingElement.name.name !== 'a') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const accessibleText = getAccessibleText(node);
|
|
153
|
+
const normalizedText = normalizeText(accessibleText);
|
|
154
|
+
const comparisonText = normalizedText.replace(/[,.?¿!‽¡;:]/g, ''); // Remove punctuation for comparison
|
|
155
|
+
// Check for exact matches (after punctuation removal)
|
|
156
|
+
const hasAmbiguousText = ambiguousWords.includes(comparisonText);
|
|
157
|
+
if (hasAmbiguousText) {
|
|
158
|
+
context.report({
|
|
159
|
+
node: node.openingElement,
|
|
160
|
+
messageId: 'ambiguousText',
|
|
161
|
+
data: {
|
|
162
|
+
text: accessibleText,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: anchor-has-content
|
|
8
|
+
* Enforce that anchors have content to be accessible to screen readers
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-has-content.md
|
|
11
|
+
*/
|
|
12
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
13
|
+
type Options = {
|
|
14
|
+
components?: string[];
|
|
15
|
+
};
|
|
16
|
+
type RuleOptions = [Options?];
|
|
17
|
+
export declare const anchorHasContent: TSESLint.RuleModule<"missingContent", RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.anchorHasContent = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
/**
|
|
12
|
+
* Check if node has accessible content
|
|
13
|
+
*/
|
|
14
|
+
function hasContent(node, children) {
|
|
15
|
+
// Check children
|
|
16
|
+
if (children.length > 0) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
// Check props (dangerouslySetInnerHTML, children prop, aria-label, title)
|
|
20
|
+
return node.attributes.some((attr) => {
|
|
21
|
+
if (attr.type !== 'JSXAttribute' || attr.name.type !== 'JSXIdentifier') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const name = attr.name.name;
|
|
25
|
+
return (name === 'dangerouslySetInnerHTML' ||
|
|
26
|
+
name === 'children' ||
|
|
27
|
+
name === 'aria-label' ||
|
|
28
|
+
name === 'aria-labelledby' ||
|
|
29
|
+
name === 'title');
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
exports.anchorHasContent = (0, eslint_devkit_2.createRule)({
|
|
33
|
+
name: 'anchor-has-content',
|
|
34
|
+
meta: {
|
|
35
|
+
type: 'problem',
|
|
36
|
+
docs: {
|
|
37
|
+
description: 'Enforce that anchors have content',
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
missingContent: (0, eslint_devkit_1.formatLLMMessage)({
|
|
41
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
42
|
+
issueName: 'Anchor Missing Content',
|
|
43
|
+
description: 'Anchor must have content',
|
|
44
|
+
severity: 'CRITICAL',
|
|
45
|
+
fix: 'Provide text content or aria-label',
|
|
46
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-has-content.md',
|
|
47
|
+
cwe: 'CWE-252'
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
schema: [
|
|
51
|
+
{
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
components: {
|
|
55
|
+
type: 'array',
|
|
56
|
+
items: { type: 'string' },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
defaultOptions: [{}],
|
|
64
|
+
create(context, [options = {}]) {
|
|
65
|
+
const { components = [] } = options ?? {};
|
|
66
|
+
const anchors = ['a', ...components];
|
|
67
|
+
return {
|
|
68
|
+
JSXElement(node) {
|
|
69
|
+
const openingElement = node.openingElement;
|
|
70
|
+
if (openingElement.name.type !== 'JSXIdentifier') {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!anchors.includes(openingElement.name.name)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!hasContent(openingElement, node.children)) {
|
|
77
|
+
context.report({
|
|
78
|
+
node: openingElement,
|
|
79
|
+
messageId: 'missingContent',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: anchor-is-valid
|
|
8
|
+
* Enforce that anchors are valid, navigable elements
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-is-valid.md
|
|
11
|
+
*/
|
|
12
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
13
|
+
type MessageIds = 'invalidHref' | 'noHref' | 'preferButton';
|
|
14
|
+
type Options = {
|
|
15
|
+
components?: string[];
|
|
16
|
+
specialLink?: string[];
|
|
17
|
+
aspects?: string[];
|
|
18
|
+
};
|
|
19
|
+
type RuleOptions = [Options?];
|
|
20
|
+
export declare const anchorIsValid: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.anchorIsValid = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
exports.anchorIsValid = (0, eslint_devkit_2.createRule)({
|
|
12
|
+
name: 'anchor-is-valid',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Enforce that anchors are valid, navigable elements',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
noHref: (0, eslint_devkit_1.formatLLMMessage)({
|
|
20
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
21
|
+
issueName: 'Anchor Missing Href',
|
|
22
|
+
description: 'Anchor missing href attribute',
|
|
23
|
+
severity: 'HIGH',
|
|
24
|
+
fix: 'Add href attribute or use <button>',
|
|
25
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-is-valid.md',
|
|
26
|
+
}),
|
|
27
|
+
invalidHref: (0, eslint_devkit_1.formatLLMMessage)({
|
|
28
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
29
|
+
issueName: 'Invalid Href',
|
|
30
|
+
description: 'Href value is not a valid URL',
|
|
31
|
+
severity: 'HIGH',
|
|
32
|
+
fix: 'Provide a valid URL or use <button>',
|
|
33
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-is-valid.md',
|
|
34
|
+
}),
|
|
35
|
+
preferButton: (0, eslint_devkit_1.formatLLMMessage)({
|
|
36
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
37
|
+
issueName: 'Prefer Button',
|
|
38
|
+
description: 'Anchor used as a button',
|
|
39
|
+
severity: 'HIGH',
|
|
40
|
+
fix: 'Use <button> element for actions',
|
|
41
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-is-valid.md',
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
schema: [
|
|
45
|
+
{
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
components: { type: 'array', items: { type: 'string' } },
|
|
49
|
+
specialLink: { type: 'array', items: { type: 'string' } },
|
|
50
|
+
aspects: { type: 'array', items: { type: 'string' } },
|
|
51
|
+
},
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
defaultOptions: [{}],
|
|
57
|
+
create(context, [options = {}]) {
|
|
58
|
+
const { components = [], specialLink = [] } = options || {};
|
|
59
|
+
const anchors = ['a', ...components];
|
|
60
|
+
return {
|
|
61
|
+
JSXOpeningElement(node) {
|
|
62
|
+
if (node.name.type !== 'JSXIdentifier' || !anchors.includes(node.name.name)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const href = node.attributes.find((attr) => attr.type === 'JSXAttribute' &&
|
|
66
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
67
|
+
attr.name.name === 'href');
|
|
68
|
+
const onClick = node.attributes.find((attr) => attr.type === 'JSXAttribute' &&
|
|
69
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
70
|
+
attr.name.name === 'onClick');
|
|
71
|
+
if (!href) {
|
|
72
|
+
// Check for special link props (like 'to' in React Router)
|
|
73
|
+
const hasSpecialLink = specialLink.some((prop) => node.attributes.some((attr) => attr.type === 'JSXAttribute' &&
|
|
74
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
75
|
+
attr.name.name === prop));
|
|
76
|
+
if (!hasSpecialLink) {
|
|
77
|
+
if (onClick) {
|
|
78
|
+
context.report({ node, messageId: 'preferButton' });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
context.report({ node, messageId: 'noHref' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (href.value?.type === 'Literal') {
|
|
87
|
+
const value = href.value.value;
|
|
88
|
+
if (value === '#' || value === 'javascript:void(0)') {
|
|
89
|
+
if (onClick) {
|
|
90
|
+
context.report({ node, messageId: 'preferButton' });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
context.report({ node, messageId: 'invalidHref' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: aria-activedescendant-has-tabindex
|
|
8
|
+
* Enforce that elements with aria-activedescendant have proper tabindex
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-activedescendant-has-tabindex.md
|
|
11
|
+
*/
|
|
12
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
13
|
+
export declare const ariaActivedescendantHasTabindex: TSESLint.RuleModule<"missingTabIndex", [], unknown, TSESLint.RuleListener> & {
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ariaActivedescendantHasTabindex = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
const INHERENTLY_FOCUSABLE = new Set([
|
|
12
|
+
'input', 'select', 'textarea', 'button', 'a', 'area'
|
|
13
|
+
]);
|
|
14
|
+
exports.ariaActivedescendantHasTabindex = (0, eslint_devkit_2.createRule)({
|
|
15
|
+
name: 'aria-activedescendant-has-tabindex',
|
|
16
|
+
meta: {
|
|
17
|
+
type: 'problem',
|
|
18
|
+
docs: {
|
|
19
|
+
description: 'Enforce that elements with aria-activedescendant have proper tabindex',
|
|
20
|
+
},
|
|
21
|
+
messages: {
|
|
22
|
+
missingTabIndex: (0, eslint_devkit_1.formatLLMMessage)({
|
|
23
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
24
|
+
issueName: 'Missing TabIndex for aria-activedescendant',
|
|
25
|
+
description: 'Elements with aria-activedescendant must be tabbable',
|
|
26
|
+
severity: 'HIGH',
|
|
27
|
+
fix: 'Add tabIndex={0} or tabIndex={-1}',
|
|
28
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-activedescendant-has-tabindex.md',
|
|
29
|
+
cwe: 'CWE-252'
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
schema: [],
|
|
33
|
+
},
|
|
34
|
+
defaultOptions: [],
|
|
35
|
+
create(context) {
|
|
36
|
+
return {
|
|
37
|
+
JSXOpeningElement(node) {
|
|
38
|
+
if (node.name.type !== 'JSXIdentifier')
|
|
39
|
+
return;
|
|
40
|
+
const element = node.name.name;
|
|
41
|
+
// Check if element has aria-activedescendant
|
|
42
|
+
const ariaActivedescendant = node.attributes.find((attr) => attr.type === 'JSXAttribute' &&
|
|
43
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
44
|
+
attr.name.name === 'aria-activedescendant');
|
|
45
|
+
if (!ariaActivedescendant)
|
|
46
|
+
return;
|
|
47
|
+
// Check if element is inherently focusable
|
|
48
|
+
if (INHERENTLY_FOCUSABLE.has(element))
|
|
49
|
+
return;
|
|
50
|
+
// Check if element has explicit tabIndex
|
|
51
|
+
const tabIndex = node.attributes.find((attr) => attr.type === 'JSXAttribute' &&
|
|
52
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
53
|
+
attr.name.name === 'tabIndex');
|
|
54
|
+
if (!tabIndex) {
|
|
55
|
+
context.report({
|
|
56
|
+
node: ariaActivedescendant,
|
|
57
|
+
messageId: 'missingTabIndex',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: aria-props
|
|
8
|
+
* Enforce that ARIA attributes are valid
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-props.md
|
|
11
|
+
*/
|
|
12
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
13
|
+
export declare const ariaProps: TSESLint.RuleModule<"invalidAriaProp", [], unknown, TSESLint.RuleListener> & {
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ariaProps = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
exports.ariaProps = (0, eslint_devkit_2.createRule)({
|
|
12
|
+
name: 'aria-props',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Enforce that ARIA attributes are valid',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
invalidAriaProp: (0, eslint_devkit_1.formatLLMMessage)({
|
|
20
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
21
|
+
issueName: 'Invalid ARIA Attribute',
|
|
22
|
+
description: 'Attribute {{name}} is not a valid ARIA attribute',
|
|
23
|
+
severity: 'HIGH',
|
|
24
|
+
fix: 'Use a valid ARIA attribute (e.g., aria-label)',
|
|
25
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-props.md',
|
|
26
|
+
cwe: 'CWE-252'
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
defaultOptions: [],
|
|
32
|
+
create(context) {
|
|
33
|
+
return {
|
|
34
|
+
JSXAttribute(node) {
|
|
35
|
+
if (node.name.type !== 'JSXIdentifier') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const name = node.name.name;
|
|
39
|
+
if (name.startsWith('aria-') && !eslint_devkit_1.ARIA_ATTRIBUTES.has(name)) {
|
|
40
|
+
context.report({
|
|
41
|
+
node,
|
|
42
|
+
messageId: 'invalidAriaProp',
|
|
43
|
+
data: {
|
|
44
|
+
name,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: aria-role
|
|
8
|
+
* Enforce that elements with ARIA roles have valid values
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-role.md
|
|
11
|
+
*/
|
|
12
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
13
|
+
type Options = {
|
|
14
|
+
allowedInvalidRoles?: string[];
|
|
15
|
+
ignoreNonDOM?: boolean;
|
|
16
|
+
};
|
|
17
|
+
type RuleOptions = [Options?];
|
|
18
|
+
export declare const ariaRole: TSESLint.RuleModule<"invalidRole", RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ariaRole = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
exports.ariaRole = (0, eslint_devkit_2.createRule)({
|
|
12
|
+
name: 'aria-role',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Enforce that elements with ARIA roles have valid values',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
invalidRole: (0, eslint_devkit_1.formatLLMMessage)({
|
|
20
|
+
icon: eslint_devkit_1.MessageIcons.ACCESSIBILITY,
|
|
21
|
+
issueName: 'Invalid ARIA Role',
|
|
22
|
+
description: 'Role "{{role}}" is not a valid ARIA role',
|
|
23
|
+
severity: 'HIGH',
|
|
24
|
+
fix: 'Use a valid ARIA role (e.g., button, alert)',
|
|
25
|
+
documentationLink: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-role.md',
|
|
26
|
+
cwe: 'CWE-252'
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
allowedInvalidRoles: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
},
|
|
37
|
+
ignoreNonDOM: {
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
defaultOptions: [{}],
|
|
46
|
+
create(context, [options = {}]) {
|
|
47
|
+
const { allowedInvalidRoles = [] } = options ?? {};
|
|
48
|
+
return {
|
|
49
|
+
JSXAttribute(node) {
|
|
50
|
+
if (node.name.type !== 'JSXIdentifier' || node.name.name !== 'role') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!node.value || node.value.type !== 'Literal' || typeof node.value.value !== 'string') {
|
|
54
|
+
// Skip dynamic values or empty values
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const roles = node.value.value.split(/\s+/);
|
|
58
|
+
for (const role of roles) {
|
|
59
|
+
if (!eslint_devkit_1.ARIA_ROLES.has(role) && !allowedInvalidRoles.includes(role)) {
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: 'invalidRole',
|
|
63
|
+
data: {
|
|
64
|
+
role,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: aria-unsupported-elements
|
|
8
|
+
* Enforce that elements that don't support ARIA roles, states, and properties do not contain them
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-unsupported-elements.md
|
|
11
|
+
*/
|
|
12
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
13
|
+
export declare const ariaUnsupportedElements: TSESLint.RuleModule<"unsupportedAria", [], unknown, TSESLint.RuleListener> & {
|
|
14
|
+
name: string;
|
|
15
|
+
};
|