eslint-plugin-a11y-enforce 0.1.0 → 0.2.1
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/CHANGELOG.md +53 -0
- package/LICENSE +1 -1
- package/README.md +8 -4
- package/dist/index.cjs +37 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +37 -23
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - Unreleased
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `input-requires-label`: no longer fires false positives on `<input type="submit">`,
|
|
12
|
+
`<input type="reset">`, `<input type="button" value="...">`, and
|
|
13
|
+
`<input type="image" alt="...">`. These input types derive their accessible name
|
|
14
|
+
from `value` or `alt` attributes, not from `aria-label` or `<label>` association.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Self-linting: `eslint.config.js` now applies TypeScript strict rules to the plugin's
|
|
18
|
+
own source code.
|
|
19
|
+
- Edge case test suite: spread attributes, dynamic values, boolean JSX expressions,
|
|
20
|
+
string numeric tabIndex, custom components, deep nesting.
|
|
21
|
+
- Multi-rule integration tests: validates rules coexist without conflict and a
|
|
22
|
+
well-structured dialog component produces zero false positives.
|
|
23
|
+
- GitHub Actions CI: typecheck, lint, test, build across Node 18, 20, 22.
|
|
24
|
+
- `CHANGELOG.md` (this file).
|
|
25
|
+
- `homepage` and `bugs` fields in `package.json`.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
- Confidence threshold in documentation raised to 90%.
|
|
29
|
+
- Removed `package-lock.json` — project uses pnpm exclusively.
|
|
30
|
+
- Copyright year updated to 2025-2026.
|
|
31
|
+
|
|
32
|
+
## [0.1.0] - 2026-04-12
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Initial release with 10 rules (6 component pattern, 4 general interaction).
|
|
36
|
+
- 149 tests across unit and flintwork integration suites.
|
|
37
|
+
- Zero runtime dependencies.
|
|
38
|
+
- ESM + CJS dual output via tsup.
|
|
39
|
+
- ESLint 8 (legacy) and ESLint 9+ (flat config) support.
|
|
40
|
+
- TypeScript source with `strict: true`, `noUncheckedIndexedAccess`,
|
|
41
|
+
`exactOptionalPropertyTypes`.
|
|
42
|
+
|
|
43
|
+
#### Rules
|
|
44
|
+
- `dialog-requires-modal` — role="dialog" must have aria-modal="true"
|
|
45
|
+
- `dialog-requires-title` — role="dialog" must have aria-labelledby or aria-label
|
|
46
|
+
- `haspopup-role-match` — aria-haspopup must be a valid ARIA value
|
|
47
|
+
- `tooltip-no-interactive` — role="tooltip" must not contain focusable children
|
|
48
|
+
- `accordion-trigger-heading` — accordion triggers must be inside headings
|
|
49
|
+
- `menuitem-not-button` — role="menuitem" should not be on button elements
|
|
50
|
+
- `focusable-has-interaction` — tabIndex={0} requires keyboard event handler
|
|
51
|
+
- `input-requires-label` — form inputs must have accessible labels
|
|
52
|
+
- `radio-group-requires-grouping` — radio buttons must be in fieldset/radiogroup
|
|
53
|
+
- `no-positive-tabindex` — tabIndex must not be greater than 0
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# eslint-plugin-a11y-enforce
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/eslint-plugin-a11y-enforce)
|
|
4
|
+
[](https://socket.dev/npm/package/eslint-plugin-a11y-enforce)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
3
7
|
ESLint plugin that catches accessibility composition errors that element-level tools miss.
|
|
4
8
|
|
|
5
9
|
`eslint-plugin-jsx-a11y` checks individual elements: "does this img have alt text?" `a11y-enforce` checks how elements relate to each other: "does this trigger's `aria-haspopup` match its content's role?" "Is this accordion trigger inside a heading?"
|
|
@@ -94,7 +98,7 @@ Elements with `role="dialog"` or `role="alertdialog"` must have `aria-modal="tru
|
|
|
94
98
|
|
|
95
99
|
#### `accordion-trigger-heading`
|
|
96
100
|
|
|
97
|
-
A `<button>` with `aria-expanded` (accordion trigger) should be inside a heading element (`h1`-`h6` or `role="heading"`). Screen reader users navigate pages by headings. Without a heading wrapper, accordion sections are invisible to heading navigation.
|
|
101
|
+
A `<button>` or element with `role="button"` that has `aria-expanded` (accordion trigger) should be inside a heading element (`h1`-`h6` or `role="heading"`). Screen reader users navigate pages by headings. Without a heading wrapper, accordion sections are invisible to heading navigation.
|
|
98
102
|
|
|
99
103
|
```jsx
|
|
100
104
|
// Bad: invisible to heading navigation
|
|
@@ -191,9 +195,9 @@ Elements with `tabIndex={0}` must have a keyboard event handler (`onKeyDown`, `o
|
|
|
191
195
|
|
|
192
196
|
## Why these rules exist
|
|
193
197
|
|
|
194
|
-
|
|
198
|
+
Over 5,000 ADA digital accessibility lawsuits were filed in 2025 across federal and state courts, up from roughly 4,000 in 2024 ([UsableNet 2025 Year-End Report](https://info.usablenet.com/2025-year-end-report-on-web-accessibility-lawsuits)). The European Accessibility Act started enforcement on June 28, 2025. India's Supreme Court declared digital access a fundamental right under Article 21 in April 2025, and SEBI mandated WCAG compliance for the financial sector in July 2025.
|
|
195
199
|
|
|
196
|
-
The
|
|
200
|
+
The composition errors this plugin catches — mismatched ARIA relationships, missing modal semantics, unlabeled dialogs, broken focus patterns — are among the most common findings in accessibility audits.
|
|
197
201
|
|
|
198
202
|
Your linter should catch these before they ship. `jsx-a11y` catches the element-level issues. `a11y-enforce` catches the composition-level issues.
|
|
199
203
|
|
|
@@ -208,7 +212,7 @@ Your linter should catch these before they ship. `jsx-a11y` catches the element-
|
|
|
208
212
|
## Stats
|
|
209
213
|
|
|
210
214
|
- 10 rules (6 component pattern, 4 general interaction)
|
|
211
|
-
-
|
|
215
|
+
- 207 tests
|
|
212
216
|
- Zero runtime dependencies
|
|
213
217
|
- ESM + CJS dual output
|
|
214
218
|
- TypeScript source with full type safety
|
package/dist/index.cjs
CHANGED
|
@@ -28,7 +28,6 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
|
|
29
29
|
// src/utils/ast-helpers.ts
|
|
30
30
|
var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
31
|
-
"a",
|
|
32
31
|
"button",
|
|
33
32
|
"input",
|
|
34
33
|
"select",
|
|
@@ -72,19 +71,27 @@ function getAttributeValue(node, attrName) {
|
|
|
72
71
|
if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
|
|
73
72
|
return attr.value.value;
|
|
74
73
|
}
|
|
75
|
-
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal"
|
|
76
|
-
|
|
74
|
+
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal") {
|
|
75
|
+
if (typeof attr.value.expression.value === "string") {
|
|
76
|
+
return attr.value.expression.value;
|
|
77
|
+
}
|
|
78
|
+
if (typeof attr.value.expression.value === "boolean") {
|
|
79
|
+
return String(attr.value.expression.value);
|
|
80
|
+
}
|
|
77
81
|
}
|
|
78
82
|
return void 0;
|
|
79
83
|
}
|
|
80
84
|
function getNumericValue(node, attrName) {
|
|
81
85
|
const attr = findAttribute(node, attrName);
|
|
82
|
-
if (!attr
|
|
86
|
+
if (!attr?.value) return void 0;
|
|
83
87
|
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "number") {
|
|
84
88
|
return attr.value.expression.value;
|
|
85
89
|
}
|
|
90
|
+
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "UnaryExpression" && attr.value.expression.operator === "-" && attr.value.expression.argument?.type === "Literal" && typeof attr.value.expression.argument.value === "number") {
|
|
91
|
+
return -attr.value.expression.argument.value;
|
|
92
|
+
}
|
|
86
93
|
if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
|
|
87
|
-
const parsed = parseInt(attr.value.value, 10);
|
|
94
|
+
const parsed = Number.parseInt(attr.value.value, 10);
|
|
88
95
|
if (!Number.isNaN(parsed)) return parsed;
|
|
89
96
|
}
|
|
90
97
|
return void 0;
|
|
@@ -101,8 +108,14 @@ function getElementType(node) {
|
|
|
101
108
|
}
|
|
102
109
|
return "";
|
|
103
110
|
}
|
|
104
|
-
function isInteractiveElement(tagName) {
|
|
105
|
-
|
|
111
|
+
function isInteractiveElement(tagName, node) {
|
|
112
|
+
const lower = tagName.toLowerCase();
|
|
113
|
+
if (INTERACTIVE_ELEMENTS.has(lower)) return true;
|
|
114
|
+
if (lower === "a") {
|
|
115
|
+
if (!node) return true;
|
|
116
|
+
return hasAttribute(node, "href");
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
106
119
|
}
|
|
107
120
|
function isHeadingElement(tagName, node) {
|
|
108
121
|
if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;
|
|
@@ -130,7 +143,7 @@ var rule = {
|
|
|
130
143
|
type: "problem",
|
|
131
144
|
docs: {
|
|
132
145
|
description: 'Enforce that elements with role="dialog" have aria-modal="true".',
|
|
133
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
146
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal"
|
|
134
147
|
},
|
|
135
148
|
messages: {
|
|
136
149
|
missingAriaModal: 'Elements with role="{{ role }}" must have aria-modal="true". Without aria-modal, screen readers will not restrict navigation to the dialog content, allowing users to accidentally interact with the page behind it. Add aria-modal="true" to this element.'
|
|
@@ -168,7 +181,7 @@ var rule2 = {
|
|
|
168
181
|
type: "problem",
|
|
169
182
|
docs: {
|
|
170
183
|
description: "Enforce that aria-haspopup has a valid ARIA value.",
|
|
171
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
184
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match"
|
|
172
185
|
},
|
|
173
186
|
messages: {
|
|
174
187
|
invalidHaspopup: 'aria-haspopup value "{{ value }}" is not valid. Allowed values are: menu, listbox, tree, grid, dialog, true, false. The value must match the role of the popup content it triggers. Screen readers use this value to announce the type of popup that will appear.'
|
|
@@ -196,7 +209,7 @@ var rule3 = {
|
|
|
196
209
|
type: "problem",
|
|
197
210
|
docs: {
|
|
198
211
|
description: "Enforce that tooltip content does not contain interactive elements.",
|
|
199
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
212
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive"
|
|
200
213
|
},
|
|
201
214
|
messages: {
|
|
202
215
|
interactiveInTooltip: 'Tooltip (role="tooltip") must not contain interactive elements. Tooltips are non-interactive by design. Users cannot Tab to content inside a tooltip because it disappears on blur. If you need interactive content in a popup, use a Popover or Dialog instead.'
|
|
@@ -216,7 +229,7 @@ var rule3 = {
|
|
|
216
229
|
if (getAttributeValue(node, "role") === "tooltip") return;
|
|
217
230
|
if (!isInsideTooltip(node)) return;
|
|
218
231
|
const tagName = getElementType(node);
|
|
219
|
-
if (isInteractiveElement(tagName)) {
|
|
232
|
+
if (isInteractiveElement(tagName, node)) {
|
|
220
233
|
context.report({ node: astNode, messageId: "interactiveInTooltip" });
|
|
221
234
|
return;
|
|
222
235
|
}
|
|
@@ -241,7 +254,7 @@ var rule4 = {
|
|
|
241
254
|
type: "problem",
|
|
242
255
|
docs: {
|
|
243
256
|
description: "Enforce that accordion trigger buttons are inside heading elements.",
|
|
244
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
257
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading"
|
|
245
258
|
},
|
|
246
259
|
messages: {
|
|
247
260
|
missingHeading: 'Accordion trigger (button with aria-expanded) should be inside a heading element (h1-h6) or an element with role="heading". Without a heading, screen reader users navigating by headings will not discover this accordion section. Wrap the button in an appropriate heading element.'
|
|
@@ -253,7 +266,8 @@ var rule4 = {
|
|
|
253
266
|
JSXOpeningElement(astNode) {
|
|
254
267
|
const node = astNode;
|
|
255
268
|
const tagName = getElementType(node);
|
|
256
|
-
|
|
269
|
+
const isButton = tagName.toLowerCase() === "button" || getAttributeValue(node, "role") === "button";
|
|
270
|
+
if (!isButton) return;
|
|
257
271
|
if (!hasAttribute(node, "aria-expanded")) return;
|
|
258
272
|
const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {
|
|
259
273
|
const ancestorTag = getElementType(ancestor);
|
|
@@ -279,7 +293,7 @@ var rule5 = {
|
|
|
279
293
|
type: "problem",
|
|
280
294
|
docs: {
|
|
281
295
|
description: 'Enforce that role="menuitem" is not used on button elements.',
|
|
282
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
296
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button"
|
|
283
297
|
},
|
|
284
298
|
messages: {
|
|
285
299
|
menuitemOnButton: 'role="{{ role }}" should not be used on <button> elements. Buttons have an implicit "button" role, which causes some screen readers to double-announce: "button, menuitem." Use a <div> or <li> with role="{{ role }}" and tabIndex={-1} instead.'
|
|
@@ -308,7 +322,7 @@ var rule6 = {
|
|
|
308
322
|
type: "problem",
|
|
309
323
|
docs: {
|
|
310
324
|
description: "Enforce that dialogs have an accessible name via aria-labelledby or aria-label.",
|
|
311
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
325
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title"
|
|
312
326
|
},
|
|
313
327
|
messages: {
|
|
314
328
|
missingDialogTitle: 'Dialog (role="{{ role }}") must have an accessible name via aria-labelledby or aria-label. Without a name, screen readers announce "dialog" with no context. Add aria-labelledby pointing to a heading inside the dialog, or aria-label with a descriptive name.'
|
|
@@ -342,7 +356,7 @@ var rule7 = {
|
|
|
342
356
|
type: "problem",
|
|
343
357
|
docs: {
|
|
344
358
|
description: "Enforce that elements with tabIndex={0} have keyboard event handlers.",
|
|
345
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
359
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction"
|
|
346
360
|
},
|
|
347
361
|
messages: {
|
|
348
362
|
missingKeyboardHandler: "Element with tabIndex={0} is focusable but has no keyboard event handler (onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this element but cannot interact with it. Add an onKeyDown handler, or remove tabIndex if the element is not meant to be interactive."
|
|
@@ -356,7 +370,7 @@ var rule7 = {
|
|
|
356
370
|
const tabIndex = getNumericValue(node, "tabIndex");
|
|
357
371
|
if (tabIndex !== 0) return;
|
|
358
372
|
const tagName = getElementType(node);
|
|
359
|
-
if (isInteractiveElement(tagName)) return;
|
|
373
|
+
if (isInteractiveElement(tagName, node)) return;
|
|
360
374
|
if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {
|
|
361
375
|
context.report({ node: astNode, messageId: "missingKeyboardHandler" });
|
|
362
376
|
}
|
|
@@ -372,7 +386,7 @@ var rule8 = {
|
|
|
372
386
|
type: "problem",
|
|
373
387
|
docs: {
|
|
374
388
|
description: "Enforce that form inputs have an accessible label.",
|
|
375
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
389
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label"
|
|
376
390
|
},
|
|
377
391
|
messages: {
|
|
378
392
|
missingLabel: 'Form input ({{ element }}) must have an accessible label. Screen readers announce inputs by their label. Without one, users hear "edit text" with no context. Add aria-label, aria-labelledby, or associate a <label> element using htmlFor. Note: placeholder is not a substitute for a label.'
|
|
@@ -386,8 +400,8 @@ var rule8 = {
|
|
|
386
400
|
const tagName = getElementType(node);
|
|
387
401
|
if (!isFormInput(tagName)) return;
|
|
388
402
|
const inputType = getAttributeValue(node, "type");
|
|
389
|
-
if (inputType === "hidden") return;
|
|
390
|
-
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id");
|
|
403
|
+
if (inputType === "hidden" || inputType === "submit" || inputType === "reset") return;
|
|
404
|
+
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id") || inputType === "button" && hasAttribute(node, "value") || inputType === "image" && hasAttribute(node, "alt");
|
|
391
405
|
if (!hasAccessibleLabel) {
|
|
392
406
|
context.report({
|
|
393
407
|
node: astNode,
|
|
@@ -407,7 +421,7 @@ var rule9 = {
|
|
|
407
421
|
type: "problem",
|
|
408
422
|
docs: {
|
|
409
423
|
description: 'Enforce that radio buttons are inside a fieldset or role="radiogroup".',
|
|
410
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
424
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping"
|
|
411
425
|
},
|
|
412
426
|
messages: {
|
|
413
427
|
missingGrouping: 'Radio buttons must be grouped inside a <fieldset> with <legend> or an element with role="radiogroup" and aria-label. Without grouping, screen readers announce each radio button independently with no indication they belong to a set.'
|
|
@@ -443,7 +457,7 @@ var rule10 = {
|
|
|
443
457
|
type: "problem",
|
|
444
458
|
docs: {
|
|
445
459
|
description: "Enforce that tabIndex is not greater than 0.",
|
|
446
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
460
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex"
|
|
447
461
|
},
|
|
448
462
|
messages: {
|
|
449
463
|
positiveTabindex: "tabIndex must not be greater than 0 (found tabIndex={{ value }}). Positive tabIndex values break the natural tab order, creating unpredictable keyboard navigation. Use tabIndex={0} to make an element focusable in DOM order, or tabIndex={-1} for programmatic focus only."
|
|
@@ -486,7 +500,7 @@ var recommendedRules = Object.fromEntries(
|
|
|
486
500
|
var plugin = {
|
|
487
501
|
meta: {
|
|
488
502
|
name: "eslint-plugin-a11y-enforce",
|
|
489
|
-
version: "0.
|
|
503
|
+
version: "0.2.0"
|
|
490
504
|
},
|
|
491
505
|
rules,
|
|
492
506
|
configs: {}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/utils/ast-helpers.ts","../src/rules/dialog-requires-modal.ts","../src/rules/haspopup-role-match.ts","../src/rules/tooltip-no-interactive.ts","../src/rules/accordion-trigger-heading.ts","../src/rules/menuitem-not-button.ts","../src/rules/dialog-requires-title.ts","../src/rules/focusable-has-interaction.ts","../src/rules/input-requires-label.ts","../src/rules/radio-group-requires-grouping.ts","../src/rules/no-positive-tabindex.ts"],"sourcesContent":["/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.1.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n","/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'a', 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'string'\n ) {\n return attr.value.expression.value;\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr || attr.value === null) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/** Native HTML elements with built-in keyboard behavior. */\nexport function isInteractiveElement(tagName: string): boolean {\n return INTERACTIVE_ELEMENTS.has(tagName.toLowerCase());\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // Cast through `unknown` because JSXElement's readonly properties don't\n // overlap with Record's index signature under exactOptionalPropertyTypes.\n let current = node.parent as unknown as Record<string, unknown> | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement as unknown as JSXOpeningElement)) return true;\n }\n current = (current.parent as unknown as Record<string, unknown> | null) ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-modal.md',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/haspopup-role-match.md',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/tooltip-no-interactive.md',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a, input, select, textarea\n if (isInteractiveElement(tagName)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> with aria-expanded (the accordion trigger pattern)\n * should be wrapped in a heading element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/accordion-trigger-heading.md',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Only applies to <button> elements with aria-expanded.\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n if (tagName.toLowerCase() !== 'button') return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/menuitem-not-button.md',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-title.md',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/focusable-has-interaction.md',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, etc.) already have\n // built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Hidden inputs (type=\"hidden\") are excluded because they have no\n * visual or accessible representation.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/input-requires-label.md',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n // Hidden inputs have no visual or accessible representation\n const inputType = getAttributeValue(node, 'type');\n if (inputType === 'hidden') return;\n\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id');\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/radio-group-requires-grouping.md',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/no-positive-tabindex.md',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBA,IAAM,uBAA4C,oBAAI,IAAI;AAAA,EACxD;AAAA,EAAK;AAAA,EAAU;AAAA,EAAS;AAAA,EAAU;AACpC,CAAC;AAED,IAAM,mBAAwC,oBAAI,IAAI;AAAA,EACpD;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAChC,CAAC;AAED,IAAM,sBAA2C,oBAAI,IAAI;AAAA,EACvD;AAAA,EAAS;AAAA,EAAU;AACrB,CAAC;AAMM,IAAM,oBAAyC,oBAAI,IAAI;AAAA,EAC5D;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAW;AAAA,EAC7B;AAAA,EAAS;AAAA,EAAY;AAAA,EAAY;AACnC,CAAC;AAQD,SAAS,cACP,MACA,UAC0B;AAC1B,aAAW,QAAQ,KAAK,YAAY;AAClC,QACE,KAAK,SAAS,kBACd,KAAK,KAAK,SAAS,mBACnB,KAAK,KAAK,SAAS,UACnB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAUO,SAAS,kBACd,MACA,UACoB;AACpB,QAAM,OAAO,cAAc,MAAM,QAAQ;AACzC,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,KAAK,UAAU,KAAM,QAAO;AAEhC,MAAI,KAAK,MAAM,SAAS,aAAa,OAAO,KAAK,MAAM,UAAU,UAAU;AACzE,WAAO,KAAK,MAAM;AAAA,EACpB;AAEA,MACE,KAAK,MAAM,SAAS,4BACpB,KAAK,MAAM,WAAW,SAAS,aAC/B,OAAO,KAAK,MAAM,WAAW,UAAU,UACvC;AACA,WAAO,KAAK,MAAM,WAAW;AAAA,EAC/B;AAEA,SAAO;AACT;AASO,SAAS,gBACd,MACA,UACoB;AACpB,QAAM,OAAO,cAAc,MAAM,QAAQ;AACzC,MAAI,CAAC,QAAQ,KAAK,UAAU,KAAM,QAAO;AAEzC,MACE,KAAK,MAAM,SAAS,4BACpB,KAAK,MAAM,WAAW,SAAS,aAC/B,OAAO,KAAK,MAAM,WAAW,UAAU,UACvC;AACA,WAAO,KAAK,MAAM,WAAW;AAAA,EAC/B;AAEA,MAAI,KAAK,MAAM,SAAS,aAAa,OAAO,KAAK,MAAM,UAAU,UAAU;AACzE,UAAM,SAAS,SAAS,KAAK,MAAM,OAAO,EAAE;AAC5C,QAAI,CAAC,OAAO,MAAM,MAAM,EAAG,QAAO;AAAA,EACpC;AAEA,SAAO;AACT;AAGO,SAAS,aACd,MACA,UACS;AACT,SAAO,cAAc,MAAM,QAAQ,MAAM;AAC3C;AAGO,SAAS,mBACd,MACA,cACS;AACT,SAAO,aAAa,KAAK,CAAC,SAAS,aAAa,MAAM,IAAI,CAAC;AAC7D;AASO,SAAS,eAAe,MAAiC;AAC9D,MAAI,KAAK,KAAK,SAAS,mBAAmB,UAAU,KAAK,MAAM;AAC7D,WAAO,KAAK,KAAK,QAAQ;AAAA,EAC3B;AACA,SAAO;AACT;AAKO,SAAS,qBAAqB,SAA0B;AAC7D,SAAO,qBAAqB,IAAI,QAAQ,YAAY,CAAC;AACvD;AAOO,SAAS,iBACd,SACA,MACS;AACT,MAAI,iBAAiB,IAAI,QAAQ,YAAY,CAAC,EAAG,QAAO;AACxD,MAAI,QAAQ,kBAAkB,MAAM,MAAM,MAAM,UAAW,QAAO;AAClE,SAAO;AACT;AAGO,SAAS,YAAY,SAA0B;AACpD,SAAO,oBAAoB,IAAI,QAAQ,YAAY,CAAC;AACtD;AAWO,SAAS,oBACd,MACA,WACS;AAKT,MAAI,UAAU,KAAK;AAEnB,SAAO,SAAS;AACd,QAAI,QAAQ,SAAS,gBAAgB,QAAQ,gBAAgB;AAC3D,UAAI,UAAU,QAAQ,cAA8C,EAAG,QAAO;AAAA,IAChF;AACA,cAAW,QAAQ,UAAwD;AAAA,EAC7E;AAEA,SAAO;AACT;;;AC9LA,IAAM,eAAsC,CAAC,UAAU,aAAa;AAEpE,IAAM,OAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,OAAO,kBAAkB,MAAM,MAAM;AAE3C,YAAI,CAAC,QAAQ,CAAC,aAAa,SAAS,IAAI,EAAG;AAE3C,cAAM,YAAY,kBAAkB,MAAM,YAAY;AACtD,YAAI,cAAc,QAAQ;AACxB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,oBAAoB,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,QACjF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gCAAQ;;;ACjCf,IAAM,wBAA6C,oBAAI,IAAI;AAAA,EACzD;AAAA,EAAQ;AAAA,EAAW;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAQ;AACvD,CAAC;AAED,IAAMA,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,iBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,WAAW,kBAAkB,MAAM,eAAe;AAExD,YAAI,aAAa,OAAW;AAE5B,YAAI,CAAC,sBAAsB,IAAI,QAAQ,GAAG;AACxC,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,mBAAmB,MAAM,EAAE,OAAO,SAAS,EAAE,CAAC;AAAA,QAC3F;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,8BAAQA;;;AC3Bf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,sBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AAMd,aAAS,gBAAgB,MAAkC;AACzD,aAAO;AAAA,QACL;AAAA,QACA,CAAC,aAAa,kBAAkB,UAAU,MAAM,MAAM;AAAA,MACxD;AAAA,IACF;AAEA,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AAGb,YAAI,kBAAkB,MAAM,MAAM,MAAM,UAAW;AACnD,YAAI,CAAC,gBAAgB,IAAI,EAAG;AAE5B,cAAM,UAAU,eAAe,IAAI;AAGnC,YAAI,qBAAqB,OAAO,GAAG;AACjC,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,uBAAuB,CAAC;AACnE;AAAA,QACF;AAGA,cAAM,WAAW,gBAAgB,MAAM,UAAU;AACjD,YAAI,aAAa,UAAa,YAAY,GAAG;AAC3C,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,uBAAuB,CAAC;AACnE;AAAA,QACF;AAGA,cAAM,YAAY,kBAAkB,MAAM,MAAM;AAChD,YAAI,aAAa,kBAAkB,IAAI,SAAS,GAAG;AACjD,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,uBAAuB,CAAC;AAAA,QACrE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iCAAQA;;;ACpEf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,gBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,UAAU,eAAe,IAAI;AAKnC,YAAI,QAAQ,YAAY,MAAM,SAAU;AACxC,YAAI,CAAC,aAAa,MAAM,eAAe,EAAG;AAE1C,cAAM,qBAAqB,oBAAoB,MAAM,CAAC,aAAa;AACjE,gBAAM,cAAc,eAAe,QAAQ;AAC3C,iBAAO,iBAAiB,aAAa,QAAQ;AAAA,QAC/C,CAAC;AAED,YAAI,CAAC,oBAAoB;AACvB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,iBAAiB,CAAC;AAAA,QAC/D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,oCAAQA;;;AC5Cf,IAAM,iBAAwC;AAAA,EAC5C;AAAA,EAAY;AAAA,EAAoB;AAClC;AAEA,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,OAAO,kBAAkB,MAAM,MAAM;AAE3C,YAAI,CAAC,QAAQ,CAAC,eAAe,SAAS,IAAI,EAAG;AAE7C,cAAM,UAAU,eAAe,IAAI;AACnC,YAAI,QAAQ,YAAY,MAAM,SAAU;AAExC,gBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,oBAAoB,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,MACjF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,8BAAQA;;;ACzCf,IAAMC,gBAAsC,CAAC,UAAU,aAAa;AAEpE,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,oBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,OAAO,kBAAkB,MAAM,MAAM;AAE3C,YAAI,CAAC,QAAQ,CAACD,cAAa,SAAS,IAAI,EAAG;AAE3C,cAAM,oBACJ,aAAa,MAAM,iBAAiB,KACpC,aAAa,MAAM,YAAY;AAEjC,YAAI,CAAC,mBAAmB;AACtB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,sBAAsB,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,QACnF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gCAAQC;;;AC5Bf,IAAM,oBAA2C;AAAA,EAC/C;AAAA,EAAa;AAAA,EAAW;AAC1B;AAEA,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,wBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,WAAW,gBAAgB,MAAM,UAAU;AAGjD,YAAI,aAAa,EAAG;AAKpB,cAAM,UAAU,eAAe,IAAI;AACnC,YAAI,qBAAqB,OAAO,EAAG;AAEnC,YAAI,CAAC,mBAAmB,MAAM,iBAAiB,GAAG;AAChD,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,yBAAyB,CAAC;AAAA,QACvE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,oCAAQA;;;AC1Cf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,cACE;AAAA,IAKJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,UAAU,eAAe,IAAI;AAEnC,YAAI,CAAC,YAAY,OAAO,EAAG;AAG3B,cAAM,YAAY,kBAAkB,MAAM,MAAM;AAChD,YAAI,cAAc,SAAU;AAE5B,cAAM,qBACJ,aAAa,MAAM,YAAY,KAC/B,aAAa,MAAM,iBAAiB,KACpC,aAAa,MAAM,IAAI;AAEzB,YAAI,CAAC,oBAAoB;AACvB,kBAAQ,OAAO;AAAA,YACb,MAAM;AAAA,YACN,WAAW;AAAA,YACX,MAAM,EAAE,SAAS,IAAI,OAAO,IAAI;AAAA,UAClC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,+BAAQA;;;ACnDf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,iBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,UAAU,eAAe,IAAI;AAEnC,YAAI,QAAQ,YAAY,MAAM,QAAS;AAEvC,cAAM,YAAY,kBAAkB,MAAM,MAAM;AAChD,YAAI,cAAc,QAAS;AAE3B,cAAM,sBAAsB,oBAAoB,MAAM,CAAC,aAAa;AAClE,gBAAM,cAAc,eAAe,QAAQ;AAC3C,cAAI,YAAY,YAAY,MAAM,WAAY,QAAO;AACrD,cAAI,kBAAkB,UAAU,MAAM,MAAM,aAAc,QAAO;AACjE,iBAAO;AAAA,QACT,CAAC;AAED,YAAI,CAAC,qBAAqB;AACxB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,kBAAkB,CAAC;AAAA,QAChE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,wCAAQA;;;ACjDf,IAAMC,SAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IAKJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,WAAW,gBAAgB,MAAM,UAAU;AAEjD,YAAI,aAAa,UAAa,YAAY,EAAG;AAE7C,gBAAQ,OAAO;AAAA,UACb,MAAM;AAAA,UACN,WAAW;AAAA,UACX,MAAM,EAAE,OAAO,OAAO,QAAQ,EAAE;AAAA,QAClC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,+BAAQA;;;AX/Bf,IAAM,QAAyC;AAAA,EAC7C,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,0BAA0B;AAAA,EAC1B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,yBAAyB;AAAA,EACzB,6BAA6B;AAAA,EAC7B,wBAAwB;AAAA,EACxB,iCAAiC;AAAA,EACjC,wBAAwB;AAC1B;AAGA,IAAM,mBAA2C,OAAO;AAAA,EACtD,OAAO,KAAK,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,gBAAgB,IAAI,IAAI,OAAO,CAAC;AACpE;AAOA,IAAM,SAAS;AAAA,EACb,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AAAA,EACA;AAAA,EACA,SAAS,CAAC;AACZ;AAKA,IAAM,kBAAkB;AAAA,EACtB,SAAS,EAAE,gBAAgB,OAAO;AAAA,EAClC,OAAO;AACT;AAIA,IAAM,oBAAoB;AAAA,EACxB,SAAS,CAAC,cAAc;AAAA,EACxB,OAAO;AACT;AAEA,OAAO,UAAU;AAAA,EACf,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,sBAAsB;AACxB;AAEA,IAAO,gBAAQ;","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/utils/ast-helpers.ts","../src/rules/dialog-requires-modal.ts","../src/rules/haspopup-role-match.ts","../src/rules/tooltip-no-interactive.ts","../src/rules/accordion-trigger-heading.ts","../src/rules/menuitem-not-button.ts","../src/rules/dialog-requires-title.ts","../src/rules/focusable-has-interaction.ts","../src/rules/input-requires-label.ts","../src/rules/radio-group-requires-grouping.ts","../src/rules/no-positive-tabindex.ts"],"sourcesContent":["/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.2.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n","/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute, ASTParentNode } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal'\n ) {\n // String expression: aria-haspopup={\"menu\"}\n if (typeof attr.value.expression.value === 'string') {\n return attr.value.expression.value;\n }\n // Boolean expression: aria-modal={true}\n // JSX boolean expressions produce Literal with boolean value.\n // Coerce to string so callers can compare against \"true\"/\"false\"\n // consistently regardless of whether the author wrote\n // aria-modal=\"true\" or aria-modal={true}.\n if (typeof attr.value.expression.value === 'boolean') {\n return String(attr.value.expression.value);\n }\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr?.value) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n // UnaryExpression: tabIndex={-1} parses as { operator: '-', argument: Literal(1) }\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'UnaryExpression' &&\n attr.value.expression.operator === '-' &&\n attr.value.expression.argument?.type === 'Literal' &&\n typeof attr.value.expression.argument.value === 'number'\n ) {\n return -attr.value.expression.argument.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = Number.parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/**\n * Native HTML elements with built-in keyboard behavior.\n *\n * <a> is only interactive when it has an href attribute. Without href,\n * it's a placeholder anchor with no keyboard behavior and no implicit\n * role. This matters for tooltip-no-interactive (an <a> without href\n * inside a tooltip is not a violation) and focusable-has-interaction\n * (an <a> without href and tabIndex={0} SHOULD fire).\n */\nexport function isInteractiveElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n const lower = tagName.toLowerCase();\n if (INTERACTIVE_ELEMENTS.has(lower)) return true;\n if (lower === 'a') {\n // If no node provided, assume interactive (conservative)\n if (!node) return true;\n return hasAttribute(node, 'href');\n }\n return false;\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // ASTParentNode types the minimal shape we need from the parent chain.\n let current: ASTParentNode | null | undefined =\n node.parent as unknown as ASTParentNode | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement)) return true;\n }\n current = current.parent ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a (with href), input, select, textarea\n if (isInteractiveElement(tagName, node)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> or element with role=\"button\" that has aria-expanded\n * (the accordion trigger pattern) should be wrapped in a heading\n * element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Applies to <button> elements and elements with role=\"button\"\n // that have aria-expanded (the accordion trigger pattern).\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n const isButton =\n tagName.toLowerCase() === 'button' ||\n getAttributeValue(node, 'role') === 'button';\n\n if (!isButton) return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, a with href) already\n // have built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName, node)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Excluded input types:\n * - type=\"hidden\": no visual or accessible representation.\n * - type=\"submit\": browser provides default accessible name (\"Submit\").\n * - type=\"reset\": browser provides default accessible name (\"Reset\").\n *\n * Special input types:\n * - type=\"button\": accessible name comes from `value` attribute.\n * - type=\"image\": accessible name comes from `alt` attribute.\n *\n * These types derive their name differently than text inputs and\n * must not trigger false positives when labeled correctly via\n * their native mechanism.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n const inputType = getAttributeValue(node, 'type');\n\n // These types have browser-provided default accessible names\n // and do not require explicit labeling.\n if (inputType === 'hidden' || inputType === 'submit' || inputType === 'reset') return;\n\n // type=\"button\" gets its name from the value attribute.\n // type=\"image\" gets its name from the alt attribute.\n // Both also accept aria-label, aria-labelledby, and id.\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id') ||\n (inputType === 'button' && hasAttribute(node, 'value')) ||\n (inputType === 'image' && hasAttribute(node, 'alt'));\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/utils/ast-helpers.ts
|
|
2
2
|
var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
3
|
-
"a",
|
|
4
3
|
"button",
|
|
5
4
|
"input",
|
|
6
5
|
"select",
|
|
@@ -44,19 +43,27 @@ function getAttributeValue(node, attrName) {
|
|
|
44
43
|
if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
|
|
45
44
|
return attr.value.value;
|
|
46
45
|
}
|
|
47
|
-
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal"
|
|
48
|
-
|
|
46
|
+
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal") {
|
|
47
|
+
if (typeof attr.value.expression.value === "string") {
|
|
48
|
+
return attr.value.expression.value;
|
|
49
|
+
}
|
|
50
|
+
if (typeof attr.value.expression.value === "boolean") {
|
|
51
|
+
return String(attr.value.expression.value);
|
|
52
|
+
}
|
|
49
53
|
}
|
|
50
54
|
return void 0;
|
|
51
55
|
}
|
|
52
56
|
function getNumericValue(node, attrName) {
|
|
53
57
|
const attr = findAttribute(node, attrName);
|
|
54
|
-
if (!attr
|
|
58
|
+
if (!attr?.value) return void 0;
|
|
55
59
|
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "number") {
|
|
56
60
|
return attr.value.expression.value;
|
|
57
61
|
}
|
|
62
|
+
if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "UnaryExpression" && attr.value.expression.operator === "-" && attr.value.expression.argument?.type === "Literal" && typeof attr.value.expression.argument.value === "number") {
|
|
63
|
+
return -attr.value.expression.argument.value;
|
|
64
|
+
}
|
|
58
65
|
if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
|
|
59
|
-
const parsed = parseInt(attr.value.value, 10);
|
|
66
|
+
const parsed = Number.parseInt(attr.value.value, 10);
|
|
60
67
|
if (!Number.isNaN(parsed)) return parsed;
|
|
61
68
|
}
|
|
62
69
|
return void 0;
|
|
@@ -73,8 +80,14 @@ function getElementType(node) {
|
|
|
73
80
|
}
|
|
74
81
|
return "";
|
|
75
82
|
}
|
|
76
|
-
function isInteractiveElement(tagName) {
|
|
77
|
-
|
|
83
|
+
function isInteractiveElement(tagName, node) {
|
|
84
|
+
const lower = tagName.toLowerCase();
|
|
85
|
+
if (INTERACTIVE_ELEMENTS.has(lower)) return true;
|
|
86
|
+
if (lower === "a") {
|
|
87
|
+
if (!node) return true;
|
|
88
|
+
return hasAttribute(node, "href");
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
78
91
|
}
|
|
79
92
|
function isHeadingElement(tagName, node) {
|
|
80
93
|
if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;
|
|
@@ -102,7 +115,7 @@ var rule = {
|
|
|
102
115
|
type: "problem",
|
|
103
116
|
docs: {
|
|
104
117
|
description: 'Enforce that elements with role="dialog" have aria-modal="true".',
|
|
105
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
118
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal"
|
|
106
119
|
},
|
|
107
120
|
messages: {
|
|
108
121
|
missingAriaModal: 'Elements with role="{{ role }}" must have aria-modal="true". Without aria-modal, screen readers will not restrict navigation to the dialog content, allowing users to accidentally interact with the page behind it. Add aria-modal="true" to this element.'
|
|
@@ -140,7 +153,7 @@ var rule2 = {
|
|
|
140
153
|
type: "problem",
|
|
141
154
|
docs: {
|
|
142
155
|
description: "Enforce that aria-haspopup has a valid ARIA value.",
|
|
143
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
156
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match"
|
|
144
157
|
},
|
|
145
158
|
messages: {
|
|
146
159
|
invalidHaspopup: 'aria-haspopup value "{{ value }}" is not valid. Allowed values are: menu, listbox, tree, grid, dialog, true, false. The value must match the role of the popup content it triggers. Screen readers use this value to announce the type of popup that will appear.'
|
|
@@ -168,7 +181,7 @@ var rule3 = {
|
|
|
168
181
|
type: "problem",
|
|
169
182
|
docs: {
|
|
170
183
|
description: "Enforce that tooltip content does not contain interactive elements.",
|
|
171
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
184
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive"
|
|
172
185
|
},
|
|
173
186
|
messages: {
|
|
174
187
|
interactiveInTooltip: 'Tooltip (role="tooltip") must not contain interactive elements. Tooltips are non-interactive by design. Users cannot Tab to content inside a tooltip because it disappears on blur. If you need interactive content in a popup, use a Popover or Dialog instead.'
|
|
@@ -188,7 +201,7 @@ var rule3 = {
|
|
|
188
201
|
if (getAttributeValue(node, "role") === "tooltip") return;
|
|
189
202
|
if (!isInsideTooltip(node)) return;
|
|
190
203
|
const tagName = getElementType(node);
|
|
191
|
-
if (isInteractiveElement(tagName)) {
|
|
204
|
+
if (isInteractiveElement(tagName, node)) {
|
|
192
205
|
context.report({ node: astNode, messageId: "interactiveInTooltip" });
|
|
193
206
|
return;
|
|
194
207
|
}
|
|
@@ -213,7 +226,7 @@ var rule4 = {
|
|
|
213
226
|
type: "problem",
|
|
214
227
|
docs: {
|
|
215
228
|
description: "Enforce that accordion trigger buttons are inside heading elements.",
|
|
216
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
229
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading"
|
|
217
230
|
},
|
|
218
231
|
messages: {
|
|
219
232
|
missingHeading: 'Accordion trigger (button with aria-expanded) should be inside a heading element (h1-h6) or an element with role="heading". Without a heading, screen reader users navigating by headings will not discover this accordion section. Wrap the button in an appropriate heading element.'
|
|
@@ -225,7 +238,8 @@ var rule4 = {
|
|
|
225
238
|
JSXOpeningElement(astNode) {
|
|
226
239
|
const node = astNode;
|
|
227
240
|
const tagName = getElementType(node);
|
|
228
|
-
|
|
241
|
+
const isButton = tagName.toLowerCase() === "button" || getAttributeValue(node, "role") === "button";
|
|
242
|
+
if (!isButton) return;
|
|
229
243
|
if (!hasAttribute(node, "aria-expanded")) return;
|
|
230
244
|
const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {
|
|
231
245
|
const ancestorTag = getElementType(ancestor);
|
|
@@ -251,7 +265,7 @@ var rule5 = {
|
|
|
251
265
|
type: "problem",
|
|
252
266
|
docs: {
|
|
253
267
|
description: 'Enforce that role="menuitem" is not used on button elements.',
|
|
254
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
268
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button"
|
|
255
269
|
},
|
|
256
270
|
messages: {
|
|
257
271
|
menuitemOnButton: 'role="{{ role }}" should not be used on <button> elements. Buttons have an implicit "button" role, which causes some screen readers to double-announce: "button, menuitem." Use a <div> or <li> with role="{{ role }}" and tabIndex={-1} instead.'
|
|
@@ -280,7 +294,7 @@ var rule6 = {
|
|
|
280
294
|
type: "problem",
|
|
281
295
|
docs: {
|
|
282
296
|
description: "Enforce that dialogs have an accessible name via aria-labelledby or aria-label.",
|
|
283
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
297
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title"
|
|
284
298
|
},
|
|
285
299
|
messages: {
|
|
286
300
|
missingDialogTitle: 'Dialog (role="{{ role }}") must have an accessible name via aria-labelledby or aria-label. Without a name, screen readers announce "dialog" with no context. Add aria-labelledby pointing to a heading inside the dialog, or aria-label with a descriptive name.'
|
|
@@ -314,7 +328,7 @@ var rule7 = {
|
|
|
314
328
|
type: "problem",
|
|
315
329
|
docs: {
|
|
316
330
|
description: "Enforce that elements with tabIndex={0} have keyboard event handlers.",
|
|
317
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
331
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction"
|
|
318
332
|
},
|
|
319
333
|
messages: {
|
|
320
334
|
missingKeyboardHandler: "Element with tabIndex={0} is focusable but has no keyboard event handler (onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this element but cannot interact with it. Add an onKeyDown handler, or remove tabIndex if the element is not meant to be interactive."
|
|
@@ -328,7 +342,7 @@ var rule7 = {
|
|
|
328
342
|
const tabIndex = getNumericValue(node, "tabIndex");
|
|
329
343
|
if (tabIndex !== 0) return;
|
|
330
344
|
const tagName = getElementType(node);
|
|
331
|
-
if (isInteractiveElement(tagName)) return;
|
|
345
|
+
if (isInteractiveElement(tagName, node)) return;
|
|
332
346
|
if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {
|
|
333
347
|
context.report({ node: astNode, messageId: "missingKeyboardHandler" });
|
|
334
348
|
}
|
|
@@ -344,7 +358,7 @@ var rule8 = {
|
|
|
344
358
|
type: "problem",
|
|
345
359
|
docs: {
|
|
346
360
|
description: "Enforce that form inputs have an accessible label.",
|
|
347
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
361
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label"
|
|
348
362
|
},
|
|
349
363
|
messages: {
|
|
350
364
|
missingLabel: 'Form input ({{ element }}) must have an accessible label. Screen readers announce inputs by their label. Without one, users hear "edit text" with no context. Add aria-label, aria-labelledby, or associate a <label> element using htmlFor. Note: placeholder is not a substitute for a label.'
|
|
@@ -358,8 +372,8 @@ var rule8 = {
|
|
|
358
372
|
const tagName = getElementType(node);
|
|
359
373
|
if (!isFormInput(tagName)) return;
|
|
360
374
|
const inputType = getAttributeValue(node, "type");
|
|
361
|
-
if (inputType === "hidden") return;
|
|
362
|
-
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id");
|
|
375
|
+
if (inputType === "hidden" || inputType === "submit" || inputType === "reset") return;
|
|
376
|
+
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id") || inputType === "button" && hasAttribute(node, "value") || inputType === "image" && hasAttribute(node, "alt");
|
|
363
377
|
if (!hasAccessibleLabel) {
|
|
364
378
|
context.report({
|
|
365
379
|
node: astNode,
|
|
@@ -379,7 +393,7 @@ var rule9 = {
|
|
|
379
393
|
type: "problem",
|
|
380
394
|
docs: {
|
|
381
395
|
description: 'Enforce that radio buttons are inside a fieldset or role="radiogroup".',
|
|
382
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
396
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping"
|
|
383
397
|
},
|
|
384
398
|
messages: {
|
|
385
399
|
missingGrouping: 'Radio buttons must be grouped inside a <fieldset> with <legend> or an element with role="radiogroup" and aria-label. Without grouping, screen readers announce each radio button independently with no indication they belong to a set.'
|
|
@@ -415,7 +429,7 @@ var rule10 = {
|
|
|
415
429
|
type: "problem",
|
|
416
430
|
docs: {
|
|
417
431
|
description: "Enforce that tabIndex is not greater than 0.",
|
|
418
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
432
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex"
|
|
419
433
|
},
|
|
420
434
|
messages: {
|
|
421
435
|
positiveTabindex: "tabIndex must not be greater than 0 (found tabIndex={{ value }}). Positive tabIndex values break the natural tab order, creating unpredictable keyboard navigation. Use tabIndex={0} to make an element focusable in DOM order, or tabIndex={-1} for programmatic focus only."
|
|
@@ -458,7 +472,7 @@ var recommendedRules = Object.fromEntries(
|
|
|
458
472
|
var plugin = {
|
|
459
473
|
meta: {
|
|
460
474
|
name: "eslint-plugin-a11y-enforce",
|
|
461
|
-
version: "0.
|
|
475
|
+
version: "0.2.0"
|
|
462
476
|
},
|
|
463
477
|
rules,
|
|
464
478
|
configs: {}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/ast-helpers.ts","../src/rules/dialog-requires-modal.ts","../src/rules/haspopup-role-match.ts","../src/rules/tooltip-no-interactive.ts","../src/rules/accordion-trigger-heading.ts","../src/rules/menuitem-not-button.ts","../src/rules/dialog-requires-title.ts","../src/rules/focusable-has-interaction.ts","../src/rules/input-requires-label.ts","../src/rules/radio-group-requires-grouping.ts","../src/rules/no-positive-tabindex.ts","../src/index.ts"],"sourcesContent":["/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'a', 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'string'\n ) {\n return attr.value.expression.value;\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr || attr.value === null) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/** Native HTML elements with built-in keyboard behavior. */\nexport function isInteractiveElement(tagName: string): boolean {\n return INTERACTIVE_ELEMENTS.has(tagName.toLowerCase());\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // Cast through `unknown` because JSXElement's readonly properties don't\n // overlap with Record's index signature under exactOptionalPropertyTypes.\n let current = node.parent as unknown as Record<string, unknown> | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement as unknown as JSXOpeningElement)) return true;\n }\n current = (current.parent as unknown as Record<string, unknown> | null) ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-modal.md',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/haspopup-role-match.md',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/tooltip-no-interactive.md',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a, input, select, textarea\n if (isInteractiveElement(tagName)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> with aria-expanded (the accordion trigger pattern)\n * should be wrapped in a heading element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/accordion-trigger-heading.md',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Only applies to <button> elements with aria-expanded.\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n if (tagName.toLowerCase() !== 'button') return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/menuitem-not-button.md',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-title.md',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/focusable-has-interaction.md',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, etc.) already have\n // built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Hidden inputs (type=\"hidden\") are excluded because they have no\n * visual or accessible representation.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/input-requires-label.md',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n // Hidden inputs have no visual or accessible representation\n const inputType = getAttributeValue(node, 'type');\n if (inputType === 'hidden') return;\n\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id');\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/radio-group-requires-grouping.md',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/no-positive-tabindex.md',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.1.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/ast-helpers.ts","../src/rules/dialog-requires-modal.ts","../src/rules/haspopup-role-match.ts","../src/rules/tooltip-no-interactive.ts","../src/rules/accordion-trigger-heading.ts","../src/rules/menuitem-not-button.ts","../src/rules/dialog-requires-title.ts","../src/rules/focusable-has-interaction.ts","../src/rules/input-requires-label.ts","../src/rules/radio-group-requires-grouping.ts","../src/rules/no-positive-tabindex.ts","../src/index.ts"],"sourcesContent":["/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute, ASTParentNode } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal'\n ) {\n // String expression: aria-haspopup={\"menu\"}\n if (typeof attr.value.expression.value === 'string') {\n return attr.value.expression.value;\n }\n // Boolean expression: aria-modal={true}\n // JSX boolean expressions produce Literal with boolean value.\n // Coerce to string so callers can compare against \"true\"/\"false\"\n // consistently regardless of whether the author wrote\n // aria-modal=\"true\" or aria-modal={true}.\n if (typeof attr.value.expression.value === 'boolean') {\n return String(attr.value.expression.value);\n }\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr?.value) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n // UnaryExpression: tabIndex={-1} parses as { operator: '-', argument: Literal(1) }\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'UnaryExpression' &&\n attr.value.expression.operator === '-' &&\n attr.value.expression.argument?.type === 'Literal' &&\n typeof attr.value.expression.argument.value === 'number'\n ) {\n return -attr.value.expression.argument.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = Number.parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/**\n * Native HTML elements with built-in keyboard behavior.\n *\n * <a> is only interactive when it has an href attribute. Without href,\n * it's a placeholder anchor with no keyboard behavior and no implicit\n * role. This matters for tooltip-no-interactive (an <a> without href\n * inside a tooltip is not a violation) and focusable-has-interaction\n * (an <a> without href and tabIndex={0} SHOULD fire).\n */\nexport function isInteractiveElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n const lower = tagName.toLowerCase();\n if (INTERACTIVE_ELEMENTS.has(lower)) return true;\n if (lower === 'a') {\n // If no node provided, assume interactive (conservative)\n if (!node) return true;\n return hasAttribute(node, 'href');\n }\n return false;\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // ASTParentNode types the minimal shape we need from the parent chain.\n let current: ASTParentNode | null | undefined =\n node.parent as unknown as ASTParentNode | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement)) return true;\n }\n current = current.parent ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a (with href), input, select, textarea\n if (isInteractiveElement(tagName, node)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> or element with role=\"button\" that has aria-expanded\n * (the accordion trigger pattern) should be wrapped in a heading\n * element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Applies to <button> elements and elements with role=\"button\"\n // that have aria-expanded (the accordion trigger pattern).\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n const isButton =\n tagName.toLowerCase() === 'button' ||\n getAttributeValue(node, 'role') === 'button';\n\n if (!isButton) return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, a with href) already\n // have built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName, node)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Excluded input types:\n * - type=\"hidden\": no visual or accessible representation.\n * - type=\"submit\": browser provides default accessible name (\"Submit\").\n * - type=\"reset\": browser provides default accessible name (\"Reset\").\n *\n * Special input types:\n * - type=\"button\": accessible name comes from `value` attribute.\n * - type=\"image\": accessible name comes from `alt` attribute.\n *\n * These types derive their name differently than text inputs and\n * must not trigger false positives when labeled correctly via\n * their native mechanism.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n const inputType = getAttributeValue(node, 'type');\n\n // These types have browser-provided default accessible names\n // and do not require explicit labeling.\n if (inputType === 'hidden' || inputType === 'submit' || inputType === 'reset') return;\n\n // type=\"button\" gets its name from the value attribute.\n // type=\"image\" gets its name from the alt attribute.\n // Both also accept aria-label, aria-labelledby, and id.\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id') ||\n (inputType === 'button' && hasAttribute(node, 'value')) ||\n (inputType === 'image' && hasAttribute(node, 'alt'));\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.2.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-a11y-enforce",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "ESLint plugin that catches accessibility composition errors that element-level tools miss.",
|
|
5
5
|
"author": "Venkatesh Mukundan <vmvenkatesh78@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce"
|
|
10
10
|
},
|
|
11
|
+
"homepage": "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce#readme",
|
|
12
|
+
"bugs": "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/issues",
|
|
11
13
|
"keywords": [
|
|
12
14
|
"eslint",
|
|
13
15
|
"eslintplugin",
|
|
@@ -33,18 +35,9 @@
|
|
|
33
35
|
"files": [
|
|
34
36
|
"dist",
|
|
35
37
|
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
36
39
|
"LICENSE"
|
|
37
40
|
],
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsup",
|
|
40
|
-
"dev": "tsup --watch",
|
|
41
|
-
"test": "vitest run",
|
|
42
|
-
"test:watch": "vitest",
|
|
43
|
-
"typecheck": "tsc --noEmit",
|
|
44
|
-
"lint": "eslint src/",
|
|
45
|
-
"clean": "rm -rf dist",
|
|
46
|
-
"prepublishOnly": "pnpm clean && pnpm build"
|
|
47
|
-
},
|
|
48
41
|
"peerDependencies": {
|
|
49
42
|
"eslint": ">=8.0.0"
|
|
50
43
|
},
|
|
@@ -60,5 +53,13 @@
|
|
|
60
53
|
"engines": {
|
|
61
54
|
"node": ">=18.0.0"
|
|
62
55
|
},
|
|
63
|
-
"
|
|
64
|
-
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"dev": "tsup --watch",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"lint": "eslint src/",
|
|
63
|
+
"clean": "rm -rf dist"
|
|
64
|
+
}
|
|
65
|
+
}
|