eslint-plugin-a11y-enforce 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/LICENSE +1 -1
- package/dist/index.cjs +25 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +25 -19
- 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/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",
|
|
@@ -101,8 +100,14 @@ function getElementType(node) {
|
|
|
101
100
|
}
|
|
102
101
|
return "";
|
|
103
102
|
}
|
|
104
|
-
function isInteractiveElement(tagName) {
|
|
105
|
-
|
|
103
|
+
function isInteractiveElement(tagName, node) {
|
|
104
|
+
const lower = tagName.toLowerCase();
|
|
105
|
+
if (INTERACTIVE_ELEMENTS.has(lower)) return true;
|
|
106
|
+
if (lower === "a") {
|
|
107
|
+
if (!node) return true;
|
|
108
|
+
return hasAttribute(node, "href");
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
106
111
|
}
|
|
107
112
|
function isHeadingElement(tagName, node) {
|
|
108
113
|
if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;
|
|
@@ -130,7 +135,7 @@ var rule = {
|
|
|
130
135
|
type: "problem",
|
|
131
136
|
docs: {
|
|
132
137
|
description: 'Enforce that elements with role="dialog" have aria-modal="true".',
|
|
133
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
138
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal"
|
|
134
139
|
},
|
|
135
140
|
messages: {
|
|
136
141
|
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 +173,7 @@ var rule2 = {
|
|
|
168
173
|
type: "problem",
|
|
169
174
|
docs: {
|
|
170
175
|
description: "Enforce that aria-haspopup has a valid ARIA value.",
|
|
171
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
176
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match"
|
|
172
177
|
},
|
|
173
178
|
messages: {
|
|
174
179
|
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 +201,7 @@ var rule3 = {
|
|
|
196
201
|
type: "problem",
|
|
197
202
|
docs: {
|
|
198
203
|
description: "Enforce that tooltip content does not contain interactive elements.",
|
|
199
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
204
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive"
|
|
200
205
|
},
|
|
201
206
|
messages: {
|
|
202
207
|
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 +221,7 @@ var rule3 = {
|
|
|
216
221
|
if (getAttributeValue(node, "role") === "tooltip") return;
|
|
217
222
|
if (!isInsideTooltip(node)) return;
|
|
218
223
|
const tagName = getElementType(node);
|
|
219
|
-
if (isInteractiveElement(tagName)) {
|
|
224
|
+
if (isInteractiveElement(tagName, node)) {
|
|
220
225
|
context.report({ node: astNode, messageId: "interactiveInTooltip" });
|
|
221
226
|
return;
|
|
222
227
|
}
|
|
@@ -241,7 +246,7 @@ var rule4 = {
|
|
|
241
246
|
type: "problem",
|
|
242
247
|
docs: {
|
|
243
248
|
description: "Enforce that accordion trigger buttons are inside heading elements.",
|
|
244
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
249
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading"
|
|
245
250
|
},
|
|
246
251
|
messages: {
|
|
247
252
|
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 +258,8 @@ var rule4 = {
|
|
|
253
258
|
JSXOpeningElement(astNode) {
|
|
254
259
|
const node = astNode;
|
|
255
260
|
const tagName = getElementType(node);
|
|
256
|
-
|
|
261
|
+
const isButton = tagName.toLowerCase() === "button" || getAttributeValue(node, "role") === "button";
|
|
262
|
+
if (!isButton) return;
|
|
257
263
|
if (!hasAttribute(node, "aria-expanded")) return;
|
|
258
264
|
const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {
|
|
259
265
|
const ancestorTag = getElementType(ancestor);
|
|
@@ -279,7 +285,7 @@ var rule5 = {
|
|
|
279
285
|
type: "problem",
|
|
280
286
|
docs: {
|
|
281
287
|
description: 'Enforce that role="menuitem" is not used on button elements.',
|
|
282
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
288
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button"
|
|
283
289
|
},
|
|
284
290
|
messages: {
|
|
285
291
|
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 +314,7 @@ var rule6 = {
|
|
|
308
314
|
type: "problem",
|
|
309
315
|
docs: {
|
|
310
316
|
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/
|
|
317
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title"
|
|
312
318
|
},
|
|
313
319
|
messages: {
|
|
314
320
|
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 +348,7 @@ var rule7 = {
|
|
|
342
348
|
type: "problem",
|
|
343
349
|
docs: {
|
|
344
350
|
description: "Enforce that elements with tabIndex={0} have keyboard event handlers.",
|
|
345
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
351
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction"
|
|
346
352
|
},
|
|
347
353
|
messages: {
|
|
348
354
|
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 +362,7 @@ var rule7 = {
|
|
|
356
362
|
const tabIndex = getNumericValue(node, "tabIndex");
|
|
357
363
|
if (tabIndex !== 0) return;
|
|
358
364
|
const tagName = getElementType(node);
|
|
359
|
-
if (isInteractiveElement(tagName)) return;
|
|
365
|
+
if (isInteractiveElement(tagName, node)) return;
|
|
360
366
|
if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {
|
|
361
367
|
context.report({ node: astNode, messageId: "missingKeyboardHandler" });
|
|
362
368
|
}
|
|
@@ -372,7 +378,7 @@ var rule8 = {
|
|
|
372
378
|
type: "problem",
|
|
373
379
|
docs: {
|
|
374
380
|
description: "Enforce that form inputs have an accessible label.",
|
|
375
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
381
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label"
|
|
376
382
|
},
|
|
377
383
|
messages: {
|
|
378
384
|
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 +392,8 @@ var rule8 = {
|
|
|
386
392
|
const tagName = getElementType(node);
|
|
387
393
|
if (!isFormInput(tagName)) return;
|
|
388
394
|
const inputType = getAttributeValue(node, "type");
|
|
389
|
-
if (inputType === "hidden") return;
|
|
390
|
-
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id");
|
|
395
|
+
if (inputType === "hidden" || inputType === "submit" || inputType === "reset") return;
|
|
396
|
+
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id") || inputType === "button" && hasAttribute(node, "value") || inputType === "image" && hasAttribute(node, "alt");
|
|
391
397
|
if (!hasAccessibleLabel) {
|
|
392
398
|
context.report({
|
|
393
399
|
node: astNode,
|
|
@@ -407,7 +413,7 @@ var rule9 = {
|
|
|
407
413
|
type: "problem",
|
|
408
414
|
docs: {
|
|
409
415
|
description: 'Enforce that radio buttons are inside a fieldset or role="radiogroup".',
|
|
410
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
416
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping"
|
|
411
417
|
},
|
|
412
418
|
messages: {
|
|
413
419
|
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 +449,7 @@ var rule10 = {
|
|
|
443
449
|
type: "problem",
|
|
444
450
|
docs: {
|
|
445
451
|
description: "Enforce that tabIndex is not greater than 0.",
|
|
446
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
452
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex"
|
|
447
453
|
},
|
|
448
454
|
messages: {
|
|
449
455
|
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 +492,7 @@ var recommendedRules = Object.fromEntries(
|
|
|
486
492
|
var plugin = {
|
|
487
493
|
meta: {
|
|
488
494
|
name: "eslint-plugin-a11y-enforce",
|
|
489
|
-
version: "0.
|
|
495
|
+
version: "0.2.0"
|
|
490
496
|
},
|
|
491
497
|
rules,
|
|
492
498
|
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":"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","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 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/**\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",
|
|
@@ -73,8 +72,14 @@ function getElementType(node) {
|
|
|
73
72
|
}
|
|
74
73
|
return "";
|
|
75
74
|
}
|
|
76
|
-
function isInteractiveElement(tagName) {
|
|
77
|
-
|
|
75
|
+
function isInteractiveElement(tagName, node) {
|
|
76
|
+
const lower = tagName.toLowerCase();
|
|
77
|
+
if (INTERACTIVE_ELEMENTS.has(lower)) return true;
|
|
78
|
+
if (lower === "a") {
|
|
79
|
+
if (!node) return true;
|
|
80
|
+
return hasAttribute(node, "href");
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
78
83
|
}
|
|
79
84
|
function isHeadingElement(tagName, node) {
|
|
80
85
|
if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;
|
|
@@ -102,7 +107,7 @@ var rule = {
|
|
|
102
107
|
type: "problem",
|
|
103
108
|
docs: {
|
|
104
109
|
description: 'Enforce that elements with role="dialog" have aria-modal="true".',
|
|
105
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
110
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal"
|
|
106
111
|
},
|
|
107
112
|
messages: {
|
|
108
113
|
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 +145,7 @@ var rule2 = {
|
|
|
140
145
|
type: "problem",
|
|
141
146
|
docs: {
|
|
142
147
|
description: "Enforce that aria-haspopup has a valid ARIA value.",
|
|
143
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
148
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match"
|
|
144
149
|
},
|
|
145
150
|
messages: {
|
|
146
151
|
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 +173,7 @@ var rule3 = {
|
|
|
168
173
|
type: "problem",
|
|
169
174
|
docs: {
|
|
170
175
|
description: "Enforce that tooltip content does not contain interactive elements.",
|
|
171
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
176
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive"
|
|
172
177
|
},
|
|
173
178
|
messages: {
|
|
174
179
|
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 +193,7 @@ var rule3 = {
|
|
|
188
193
|
if (getAttributeValue(node, "role") === "tooltip") return;
|
|
189
194
|
if (!isInsideTooltip(node)) return;
|
|
190
195
|
const tagName = getElementType(node);
|
|
191
|
-
if (isInteractiveElement(tagName)) {
|
|
196
|
+
if (isInteractiveElement(tagName, node)) {
|
|
192
197
|
context.report({ node: astNode, messageId: "interactiveInTooltip" });
|
|
193
198
|
return;
|
|
194
199
|
}
|
|
@@ -213,7 +218,7 @@ var rule4 = {
|
|
|
213
218
|
type: "problem",
|
|
214
219
|
docs: {
|
|
215
220
|
description: "Enforce that accordion trigger buttons are inside heading elements.",
|
|
216
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
221
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading"
|
|
217
222
|
},
|
|
218
223
|
messages: {
|
|
219
224
|
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 +230,8 @@ var rule4 = {
|
|
|
225
230
|
JSXOpeningElement(astNode) {
|
|
226
231
|
const node = astNode;
|
|
227
232
|
const tagName = getElementType(node);
|
|
228
|
-
|
|
233
|
+
const isButton = tagName.toLowerCase() === "button" || getAttributeValue(node, "role") === "button";
|
|
234
|
+
if (!isButton) return;
|
|
229
235
|
if (!hasAttribute(node, "aria-expanded")) return;
|
|
230
236
|
const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {
|
|
231
237
|
const ancestorTag = getElementType(ancestor);
|
|
@@ -251,7 +257,7 @@ var rule5 = {
|
|
|
251
257
|
type: "problem",
|
|
252
258
|
docs: {
|
|
253
259
|
description: 'Enforce that role="menuitem" is not used on button elements.',
|
|
254
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
260
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button"
|
|
255
261
|
},
|
|
256
262
|
messages: {
|
|
257
263
|
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 +286,7 @@ var rule6 = {
|
|
|
280
286
|
type: "problem",
|
|
281
287
|
docs: {
|
|
282
288
|
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/
|
|
289
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title"
|
|
284
290
|
},
|
|
285
291
|
messages: {
|
|
286
292
|
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 +320,7 @@ var rule7 = {
|
|
|
314
320
|
type: "problem",
|
|
315
321
|
docs: {
|
|
316
322
|
description: "Enforce that elements with tabIndex={0} have keyboard event handlers.",
|
|
317
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
323
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction"
|
|
318
324
|
},
|
|
319
325
|
messages: {
|
|
320
326
|
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 +334,7 @@ var rule7 = {
|
|
|
328
334
|
const tabIndex = getNumericValue(node, "tabIndex");
|
|
329
335
|
if (tabIndex !== 0) return;
|
|
330
336
|
const tagName = getElementType(node);
|
|
331
|
-
if (isInteractiveElement(tagName)) return;
|
|
337
|
+
if (isInteractiveElement(tagName, node)) return;
|
|
332
338
|
if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {
|
|
333
339
|
context.report({ node: astNode, messageId: "missingKeyboardHandler" });
|
|
334
340
|
}
|
|
@@ -344,7 +350,7 @@ var rule8 = {
|
|
|
344
350
|
type: "problem",
|
|
345
351
|
docs: {
|
|
346
352
|
description: "Enforce that form inputs have an accessible label.",
|
|
347
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
353
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label"
|
|
348
354
|
},
|
|
349
355
|
messages: {
|
|
350
356
|
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 +364,8 @@ var rule8 = {
|
|
|
358
364
|
const tagName = getElementType(node);
|
|
359
365
|
if (!isFormInput(tagName)) return;
|
|
360
366
|
const inputType = getAttributeValue(node, "type");
|
|
361
|
-
if (inputType === "hidden") return;
|
|
362
|
-
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id");
|
|
367
|
+
if (inputType === "hidden" || inputType === "submit" || inputType === "reset") return;
|
|
368
|
+
const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id") || inputType === "button" && hasAttribute(node, "value") || inputType === "image" && hasAttribute(node, "alt");
|
|
363
369
|
if (!hasAccessibleLabel) {
|
|
364
370
|
context.report({
|
|
365
371
|
node: astNode,
|
|
@@ -379,7 +385,7 @@ var rule9 = {
|
|
|
379
385
|
type: "problem",
|
|
380
386
|
docs: {
|
|
381
387
|
description: 'Enforce that radio buttons are inside a fieldset or role="radiogroup".',
|
|
382
|
-
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/
|
|
388
|
+
url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping"
|
|
383
389
|
},
|
|
384
390
|
messages: {
|
|
385
391
|
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 +421,7 @@ var rule10 = {
|
|
|
415
421
|
type: "problem",
|
|
416
422
|
docs: {
|
|
417
423
|
description: "Enforce that tabIndex is not greater than 0.",
|
|
418
|
-
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#no-positive-tabindex"
|
|
419
425
|
},
|
|
420
426
|
messages: {
|
|
421
427
|
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 +464,7 @@ var recommendedRules = Object.fromEntries(
|
|
|
458
464
|
var plugin = {
|
|
459
465
|
meta: {
|
|
460
466
|
name: "eslint-plugin-a11y-enforce",
|
|
461
|
-
version: "0.
|
|
467
|
+
version: "0.2.0"
|
|
462
468
|
},
|
|
463
469
|
rules,
|
|
464
470
|
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 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/**\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.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
+
}
|