eslint-plugin-a11y-enforce 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md 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
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Venkatesh Mukundan
3
+ Copyright (c) 2025-2026 Venkatesh Mukundan
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # eslint-plugin-a11y-enforce
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/eslint-plugin-a11y-enforce)](https://www.npmjs.com/package/eslint-plugin-a11y-enforce)
4
+ [![Socket Badge](https://socket.dev/api/badge/npm/package/eslint-plugin-a11y-enforce)](https://socket.dev/npm/package/eslint-plugin-a11y-enforce)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  ESLint plugin that catches accessibility composition errors that element-level tools miss.
4
8
 
5
9
  `eslint-plugin-jsx-a11y` checks individual elements: "does this img have alt text?" `a11y-enforce` checks how elements relate to each other: "does this trigger's `aria-haspopup` match its content's role?" "Is this accordion trigger inside a heading?"
@@ -94,7 +98,7 @@ Elements with `role="dialog"` or `role="alertdialog"` must have `aria-modal="tru
94
98
 
95
99
  #### `accordion-trigger-heading`
96
100
 
97
- A `<button>` with `aria-expanded` (accordion trigger) should be inside a heading element (`h1`-`h6` or `role="heading"`). Screen reader users navigate pages by headings. Without a heading wrapper, accordion sections are invisible to heading navigation.
101
+ A `<button>` or element with `role="button"` that has `aria-expanded` (accordion trigger) should be inside a heading element (`h1`-`h6` or `role="heading"`). Screen reader users navigate pages by headings. Without a heading wrapper, accordion sections are invisible to heading navigation.
98
102
 
99
103
  ```jsx
100
104
  // Bad: invisible to heading navigation
@@ -191,9 +195,9 @@ Elements with `tabIndex={0}` must have a keyboard event handler (`onKeyDown`, `o
191
195
 
192
196
  ## Why these rules exist
193
197
 
194
- Accessibility lawsuits in the US increased 37% in 2025, with over 5,000 federal cases filed. The European Accessibility Act started enforcement in June 2025. India's Supreme Court declared digital access a fundamental right, and SEBI mandated accessibility compliance for the financial sector with deadlines through 2026.
198
+ Over 5,000 ADA digital accessibility lawsuits were filed in 2025 across federal and state courts, up from roughly 4,000 in 2024 ([UsableNet 2025 Year-End Report](https://info.usablenet.com/2025-year-end-report-on-web-accessibility-lawsuits)). The European Accessibility Act started enforcement on June 28, 2025. India's Supreme Court declared digital access a fundamental right under Article 21 in April 2025, and SEBI mandated WCAG compliance for the financial sector in July 2025.
195
199
 
196
- The most common issues cited in audits and lawsuits are WCAG 4.1.2 (Name, Role, Value) and 1.3.1 (Info and Relationships). These are exactly the composition errors this plugin catches: mismatched ARIA relationships, missing modal semantics, unlabeled dialogs, and broken focus patterns.
200
+ The composition errors this plugin catches mismatched ARIA relationships, missing modal semantics, unlabeled dialogs, broken focus patterns — are among the most common findings in accessibility audits.
197
201
 
198
202
  Your linter should catch these before they ship. `jsx-a11y` catches the element-level issues. `a11y-enforce` catches the composition-level issues.
199
203
 
@@ -208,7 +212,7 @@ Your linter should catch these before they ship. `jsx-a11y` catches the element-
208
212
  ## Stats
209
213
 
210
214
  - 10 rules (6 component pattern, 4 general interaction)
211
- - 149 tests
215
+ - 207 tests
212
216
  - Zero runtime dependencies
213
217
  - ESM + CJS dual output
214
218
  - TypeScript source with full type safety
package/dist/index.cjs CHANGED
@@ -28,7 +28,6 @@ module.exports = __toCommonJS(index_exports);
28
28
 
29
29
  // src/utils/ast-helpers.ts
30
30
  var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([
31
- "a",
32
31
  "button",
33
32
  "input",
34
33
  "select",
@@ -72,19 +71,27 @@ function getAttributeValue(node, attrName) {
72
71
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
73
72
  return attr.value.value;
74
73
  }
75
- if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "string") {
76
- return attr.value.expression.value;
74
+ if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal") {
75
+ if (typeof attr.value.expression.value === "string") {
76
+ return attr.value.expression.value;
77
+ }
78
+ if (typeof attr.value.expression.value === "boolean") {
79
+ return String(attr.value.expression.value);
80
+ }
77
81
  }
78
82
  return void 0;
79
83
  }
80
84
  function getNumericValue(node, attrName) {
81
85
  const attr = findAttribute(node, attrName);
82
- if (!attr || attr.value === null) return void 0;
86
+ if (!attr?.value) return void 0;
83
87
  if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "number") {
84
88
  return attr.value.expression.value;
85
89
  }
90
+ if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "UnaryExpression" && attr.value.expression.operator === "-" && attr.value.expression.argument?.type === "Literal" && typeof attr.value.expression.argument.value === "number") {
91
+ return -attr.value.expression.argument.value;
92
+ }
86
93
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
87
- const parsed = parseInt(attr.value.value, 10);
94
+ const parsed = Number.parseInt(attr.value.value, 10);
88
95
  if (!Number.isNaN(parsed)) return parsed;
89
96
  }
90
97
  return void 0;
@@ -101,8 +108,14 @@ function getElementType(node) {
101
108
  }
102
109
  return "";
103
110
  }
104
- function isInteractiveElement(tagName) {
105
- return INTERACTIVE_ELEMENTS.has(tagName.toLowerCase());
111
+ function isInteractiveElement(tagName, node) {
112
+ const lower = tagName.toLowerCase();
113
+ if (INTERACTIVE_ELEMENTS.has(lower)) return true;
114
+ if (lower === "a") {
115
+ if (!node) return true;
116
+ return hasAttribute(node, "href");
117
+ }
118
+ return false;
106
119
  }
107
120
  function isHeadingElement(tagName, node) {
108
121
  if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;
@@ -130,7 +143,7 @@ var rule = {
130
143
  type: "problem",
131
144
  docs: {
132
145
  description: 'Enforce that elements with role="dialog" have aria-modal="true".',
133
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-modal.md"
146
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal"
134
147
  },
135
148
  messages: {
136
149
  missingAriaModal: 'Elements with role="{{ role }}" must have aria-modal="true". Without aria-modal, screen readers will not restrict navigation to the dialog content, allowing users to accidentally interact with the page behind it. Add aria-modal="true" to this element.'
@@ -168,7 +181,7 @@ var rule2 = {
168
181
  type: "problem",
169
182
  docs: {
170
183
  description: "Enforce that aria-haspopup has a valid ARIA value.",
171
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/haspopup-role-match.md"
184
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match"
172
185
  },
173
186
  messages: {
174
187
  invalidHaspopup: 'aria-haspopup value "{{ value }}" is not valid. Allowed values are: menu, listbox, tree, grid, dialog, true, false. The value must match the role of the popup content it triggers. Screen readers use this value to announce the type of popup that will appear.'
@@ -196,7 +209,7 @@ var rule3 = {
196
209
  type: "problem",
197
210
  docs: {
198
211
  description: "Enforce that tooltip content does not contain interactive elements.",
199
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/tooltip-no-interactive.md"
212
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive"
200
213
  },
201
214
  messages: {
202
215
  interactiveInTooltip: 'Tooltip (role="tooltip") must not contain interactive elements. Tooltips are non-interactive by design. Users cannot Tab to content inside a tooltip because it disappears on blur. If you need interactive content in a popup, use a Popover or Dialog instead.'
@@ -216,7 +229,7 @@ var rule3 = {
216
229
  if (getAttributeValue(node, "role") === "tooltip") return;
217
230
  if (!isInsideTooltip(node)) return;
218
231
  const tagName = getElementType(node);
219
- if (isInteractiveElement(tagName)) {
232
+ if (isInteractiveElement(tagName, node)) {
220
233
  context.report({ node: astNode, messageId: "interactiveInTooltip" });
221
234
  return;
222
235
  }
@@ -241,7 +254,7 @@ var rule4 = {
241
254
  type: "problem",
242
255
  docs: {
243
256
  description: "Enforce that accordion trigger buttons are inside heading elements.",
244
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/accordion-trigger-heading.md"
257
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading"
245
258
  },
246
259
  messages: {
247
260
  missingHeading: 'Accordion trigger (button with aria-expanded) should be inside a heading element (h1-h6) or an element with role="heading". Without a heading, screen reader users navigating by headings will not discover this accordion section. Wrap the button in an appropriate heading element.'
@@ -253,7 +266,8 @@ var rule4 = {
253
266
  JSXOpeningElement(astNode) {
254
267
  const node = astNode;
255
268
  const tagName = getElementType(node);
256
- if (tagName.toLowerCase() !== "button") return;
269
+ const isButton = tagName.toLowerCase() === "button" || getAttributeValue(node, "role") === "button";
270
+ if (!isButton) return;
257
271
  if (!hasAttribute(node, "aria-expanded")) return;
258
272
  const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {
259
273
  const ancestorTag = getElementType(ancestor);
@@ -279,7 +293,7 @@ var rule5 = {
279
293
  type: "problem",
280
294
  docs: {
281
295
  description: 'Enforce that role="menuitem" is not used on button elements.',
282
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/menuitem-not-button.md"
296
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button"
283
297
  },
284
298
  messages: {
285
299
  menuitemOnButton: 'role="{{ role }}" should not be used on <button> elements. Buttons have an implicit "button" role, which causes some screen readers to double-announce: "button, menuitem." Use a <div> or <li> with role="{{ role }}" and tabIndex={-1} instead.'
@@ -308,7 +322,7 @@ var rule6 = {
308
322
  type: "problem",
309
323
  docs: {
310
324
  description: "Enforce that dialogs have an accessible name via aria-labelledby or aria-label.",
311
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-title.md"
325
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title"
312
326
  },
313
327
  messages: {
314
328
  missingDialogTitle: 'Dialog (role="{{ role }}") must have an accessible name via aria-labelledby or aria-label. Without a name, screen readers announce "dialog" with no context. Add aria-labelledby pointing to a heading inside the dialog, or aria-label with a descriptive name.'
@@ -342,7 +356,7 @@ var rule7 = {
342
356
  type: "problem",
343
357
  docs: {
344
358
  description: "Enforce that elements with tabIndex={0} have keyboard event handlers.",
345
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/focusable-has-interaction.md"
359
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction"
346
360
  },
347
361
  messages: {
348
362
  missingKeyboardHandler: "Element with tabIndex={0} is focusable but has no keyboard event handler (onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this element but cannot interact with it. Add an onKeyDown handler, or remove tabIndex if the element is not meant to be interactive."
@@ -356,7 +370,7 @@ var rule7 = {
356
370
  const tabIndex = getNumericValue(node, "tabIndex");
357
371
  if (tabIndex !== 0) return;
358
372
  const tagName = getElementType(node);
359
- if (isInteractiveElement(tagName)) return;
373
+ if (isInteractiveElement(tagName, node)) return;
360
374
  if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {
361
375
  context.report({ node: astNode, messageId: "missingKeyboardHandler" });
362
376
  }
@@ -372,7 +386,7 @@ var rule8 = {
372
386
  type: "problem",
373
387
  docs: {
374
388
  description: "Enforce that form inputs have an accessible label.",
375
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/input-requires-label.md"
389
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label"
376
390
  },
377
391
  messages: {
378
392
  missingLabel: 'Form input ({{ element }}) must have an accessible label. Screen readers announce inputs by their label. Without one, users hear "edit text" with no context. Add aria-label, aria-labelledby, or associate a <label> element using htmlFor. Note: placeholder is not a substitute for a label.'
@@ -386,8 +400,8 @@ var rule8 = {
386
400
  const tagName = getElementType(node);
387
401
  if (!isFormInput(tagName)) return;
388
402
  const inputType = getAttributeValue(node, "type");
389
- if (inputType === "hidden") return;
390
- const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id");
403
+ if (inputType === "hidden" || inputType === "submit" || inputType === "reset") return;
404
+ const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id") || inputType === "button" && hasAttribute(node, "value") || inputType === "image" && hasAttribute(node, "alt");
391
405
  if (!hasAccessibleLabel) {
392
406
  context.report({
393
407
  node: astNode,
@@ -407,7 +421,7 @@ var rule9 = {
407
421
  type: "problem",
408
422
  docs: {
409
423
  description: 'Enforce that radio buttons are inside a fieldset or role="radiogroup".',
410
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/radio-group-requires-grouping.md"
424
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping"
411
425
  },
412
426
  messages: {
413
427
  missingGrouping: 'Radio buttons must be grouped inside a <fieldset> with <legend> or an element with role="radiogroup" and aria-label. Without grouping, screen readers announce each radio button independently with no indication they belong to a set.'
@@ -443,7 +457,7 @@ var rule10 = {
443
457
  type: "problem",
444
458
  docs: {
445
459
  description: "Enforce that tabIndex is not greater than 0.",
446
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/no-positive-tabindex.md"
460
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex"
447
461
  },
448
462
  messages: {
449
463
  positiveTabindex: "tabIndex must not be greater than 0 (found tabIndex={{ value }}). Positive tabIndex values break the natural tab order, creating unpredictable keyboard navigation. Use tabIndex={0} to make an element focusable in DOM order, or tabIndex={-1} for programmatic focus only."
@@ -486,7 +500,7 @@ var recommendedRules = Object.fromEntries(
486
500
  var plugin = {
487
501
  meta: {
488
502
  name: "eslint-plugin-a11y-enforce",
489
- version: "0.1.0"
503
+ version: "0.2.0"
490
504
  },
491
505
  rules,
492
506
  configs: {}
@@ -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 ) {\n // String expression: aria-haspopup={\"menu\"}\n if (typeof attr.value.expression.value === 'string') {\n return attr.value.expression.value;\n }\n // Boolean expression: aria-modal={true}\n // JSX boolean expressions produce Literal with boolean value.\n // Coerce to string so callers can compare against \"true\"/\"false\"\n // consistently regardless of whether the author wrote\n // aria-modal=\"true\" or aria-modal={true}.\n if (typeof attr.value.expression.value === 'boolean') {\n return String(attr.value.expression.value);\n }\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr?.value) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n // UnaryExpression: tabIndex={-1} parses as { operator: '-', argument: Literal(1) }\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'UnaryExpression' &&\n attr.value.expression.operator === '-' &&\n attr.value.expression.argument?.type === 'Literal' &&\n typeof attr.value.expression.argument.value === 'number'\n ) {\n return -attr.value.expression.argument.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = Number.parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/**\n * Native HTML elements with built-in keyboard behavior.\n *\n * <a> is only interactive when it has an href attribute. Without href,\n * it's a placeholder anchor with no keyboard behavior and no implicit\n * role. This matters for tooltip-no-interactive (an <a> without href\n * inside a tooltip is not a violation) and focusable-has-interaction\n * (an <a> without href and tabIndex={0} SHOULD fire).\n */\nexport function isInteractiveElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n const lower = tagName.toLowerCase();\n if (INTERACTIVE_ELEMENTS.has(lower)) return true;\n if (lower === 'a') {\n // If no node provided, assume interactive (conservative)\n if (!node) return true;\n return hasAttribute(node, 'href');\n }\n return false;\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // ASTParentNode types the minimal shape we need from the parent chain.\n let current: ASTParentNode | null | undefined =\n node.parent as unknown as ASTParentNode | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement)) return true;\n }\n current = current.parent ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a (with href), input, select, textarea\n if (isInteractiveElement(tagName, node)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> or element with role=\"button\" that has aria-expanded\n * (the accordion trigger pattern) should be wrapped in a heading\n * element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Applies to <button> elements and elements with role=\"button\"\n // that have aria-expanded (the accordion trigger pattern).\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n const isButton =\n tagName.toLowerCase() === 'button' ||\n getAttributeValue(node, 'role') === 'button';\n\n if (!isButton) return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, a with href) already\n // have built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName, node)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Excluded input types:\n * - type=\"hidden\": no visual or accessible representation.\n * - type=\"submit\": browser provides default accessible name (\"Submit\").\n * - type=\"reset\": browser provides default accessible name (\"Reset\").\n *\n * Special input types:\n * - type=\"button\": accessible name comes from `value` attribute.\n * - type=\"image\": accessible name comes from `alt` attribute.\n *\n * These types derive their name differently than text inputs and\n * must not trigger false positives when labeled correctly via\n * their native mechanism.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n const inputType = getAttributeValue(node, 'type');\n\n // These types have browser-provided default accessible names\n // and do not require explicit labeling.\n if (inputType === 'hidden' || inputType === 'submit' || inputType === 'reset') return;\n\n // type=\"button\" gets its name from the value attribute.\n // type=\"image\" gets its name from the alt attribute.\n // Both also accept aria-label, aria-labelledby, and id.\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id') ||\n (inputType === 'button' && hasAttribute(node, 'value')) ||\n (inputType === 'image' && hasAttribute(node, 'alt'));\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
package/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // src/utils/ast-helpers.ts
2
2
  var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([
3
- "a",
4
3
  "button",
5
4
  "input",
6
5
  "select",
@@ -44,19 +43,27 @@ function getAttributeValue(node, attrName) {
44
43
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
45
44
  return attr.value.value;
46
45
  }
47
- if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "string") {
48
- return attr.value.expression.value;
46
+ if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal") {
47
+ if (typeof attr.value.expression.value === "string") {
48
+ return attr.value.expression.value;
49
+ }
50
+ if (typeof attr.value.expression.value === "boolean") {
51
+ return String(attr.value.expression.value);
52
+ }
49
53
  }
50
54
  return void 0;
51
55
  }
52
56
  function getNumericValue(node, attrName) {
53
57
  const attr = findAttribute(node, attrName);
54
- if (!attr || attr.value === null) return void 0;
58
+ if (!attr?.value) return void 0;
55
59
  if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "number") {
56
60
  return attr.value.expression.value;
57
61
  }
62
+ if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "UnaryExpression" && attr.value.expression.operator === "-" && attr.value.expression.argument?.type === "Literal" && typeof attr.value.expression.argument.value === "number") {
63
+ return -attr.value.expression.argument.value;
64
+ }
58
65
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
59
- const parsed = parseInt(attr.value.value, 10);
66
+ const parsed = Number.parseInt(attr.value.value, 10);
60
67
  if (!Number.isNaN(parsed)) return parsed;
61
68
  }
62
69
  return void 0;
@@ -73,8 +80,14 @@ function getElementType(node) {
73
80
  }
74
81
  return "";
75
82
  }
76
- function isInteractiveElement(tagName) {
77
- return INTERACTIVE_ELEMENTS.has(tagName.toLowerCase());
83
+ function isInteractiveElement(tagName, node) {
84
+ const lower = tagName.toLowerCase();
85
+ if (INTERACTIVE_ELEMENTS.has(lower)) return true;
86
+ if (lower === "a") {
87
+ if (!node) return true;
88
+ return hasAttribute(node, "href");
89
+ }
90
+ return false;
78
91
  }
79
92
  function isHeadingElement(tagName, node) {
80
93
  if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;
@@ -102,7 +115,7 @@ var rule = {
102
115
  type: "problem",
103
116
  docs: {
104
117
  description: 'Enforce that elements with role="dialog" have aria-modal="true".',
105
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-modal.md"
118
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal"
106
119
  },
107
120
  messages: {
108
121
  missingAriaModal: 'Elements with role="{{ role }}" must have aria-modal="true". Without aria-modal, screen readers will not restrict navigation to the dialog content, allowing users to accidentally interact with the page behind it. Add aria-modal="true" to this element.'
@@ -140,7 +153,7 @@ var rule2 = {
140
153
  type: "problem",
141
154
  docs: {
142
155
  description: "Enforce that aria-haspopup has a valid ARIA value.",
143
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/haspopup-role-match.md"
156
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match"
144
157
  },
145
158
  messages: {
146
159
  invalidHaspopup: 'aria-haspopup value "{{ value }}" is not valid. Allowed values are: menu, listbox, tree, grid, dialog, true, false. The value must match the role of the popup content it triggers. Screen readers use this value to announce the type of popup that will appear.'
@@ -168,7 +181,7 @@ var rule3 = {
168
181
  type: "problem",
169
182
  docs: {
170
183
  description: "Enforce that tooltip content does not contain interactive elements.",
171
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/tooltip-no-interactive.md"
184
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive"
172
185
  },
173
186
  messages: {
174
187
  interactiveInTooltip: 'Tooltip (role="tooltip") must not contain interactive elements. Tooltips are non-interactive by design. Users cannot Tab to content inside a tooltip because it disappears on blur. If you need interactive content in a popup, use a Popover or Dialog instead.'
@@ -188,7 +201,7 @@ var rule3 = {
188
201
  if (getAttributeValue(node, "role") === "tooltip") return;
189
202
  if (!isInsideTooltip(node)) return;
190
203
  const tagName = getElementType(node);
191
- if (isInteractiveElement(tagName)) {
204
+ if (isInteractiveElement(tagName, node)) {
192
205
  context.report({ node: astNode, messageId: "interactiveInTooltip" });
193
206
  return;
194
207
  }
@@ -213,7 +226,7 @@ var rule4 = {
213
226
  type: "problem",
214
227
  docs: {
215
228
  description: "Enforce that accordion trigger buttons are inside heading elements.",
216
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/accordion-trigger-heading.md"
229
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading"
217
230
  },
218
231
  messages: {
219
232
  missingHeading: 'Accordion trigger (button with aria-expanded) should be inside a heading element (h1-h6) or an element with role="heading". Without a heading, screen reader users navigating by headings will not discover this accordion section. Wrap the button in an appropriate heading element.'
@@ -225,7 +238,8 @@ var rule4 = {
225
238
  JSXOpeningElement(astNode) {
226
239
  const node = astNode;
227
240
  const tagName = getElementType(node);
228
- if (tagName.toLowerCase() !== "button") return;
241
+ const isButton = tagName.toLowerCase() === "button" || getAttributeValue(node, "role") === "button";
242
+ if (!isButton) return;
229
243
  if (!hasAttribute(node, "aria-expanded")) return;
230
244
  const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {
231
245
  const ancestorTag = getElementType(ancestor);
@@ -251,7 +265,7 @@ var rule5 = {
251
265
  type: "problem",
252
266
  docs: {
253
267
  description: 'Enforce that role="menuitem" is not used on button elements.',
254
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/menuitem-not-button.md"
268
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button"
255
269
  },
256
270
  messages: {
257
271
  menuitemOnButton: 'role="{{ role }}" should not be used on <button> elements. Buttons have an implicit "button" role, which causes some screen readers to double-announce: "button, menuitem." Use a <div> or <li> with role="{{ role }}" and tabIndex={-1} instead.'
@@ -280,7 +294,7 @@ var rule6 = {
280
294
  type: "problem",
281
295
  docs: {
282
296
  description: "Enforce that dialogs have an accessible name via aria-labelledby or aria-label.",
283
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-title.md"
297
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title"
284
298
  },
285
299
  messages: {
286
300
  missingDialogTitle: 'Dialog (role="{{ role }}") must have an accessible name via aria-labelledby or aria-label. Without a name, screen readers announce "dialog" with no context. Add aria-labelledby pointing to a heading inside the dialog, or aria-label with a descriptive name.'
@@ -314,7 +328,7 @@ var rule7 = {
314
328
  type: "problem",
315
329
  docs: {
316
330
  description: "Enforce that elements with tabIndex={0} have keyboard event handlers.",
317
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/focusable-has-interaction.md"
331
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction"
318
332
  },
319
333
  messages: {
320
334
  missingKeyboardHandler: "Element with tabIndex={0} is focusable but has no keyboard event handler (onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this element but cannot interact with it. Add an onKeyDown handler, or remove tabIndex if the element is not meant to be interactive."
@@ -328,7 +342,7 @@ var rule7 = {
328
342
  const tabIndex = getNumericValue(node, "tabIndex");
329
343
  if (tabIndex !== 0) return;
330
344
  const tagName = getElementType(node);
331
- if (isInteractiveElement(tagName)) return;
345
+ if (isInteractiveElement(tagName, node)) return;
332
346
  if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {
333
347
  context.report({ node: astNode, messageId: "missingKeyboardHandler" });
334
348
  }
@@ -344,7 +358,7 @@ var rule8 = {
344
358
  type: "problem",
345
359
  docs: {
346
360
  description: "Enforce that form inputs have an accessible label.",
347
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/input-requires-label.md"
361
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label"
348
362
  },
349
363
  messages: {
350
364
  missingLabel: 'Form input ({{ element }}) must have an accessible label. Screen readers announce inputs by their label. Without one, users hear "edit text" with no context. Add aria-label, aria-labelledby, or associate a <label> element using htmlFor. Note: placeholder is not a substitute for a label.'
@@ -358,8 +372,8 @@ var rule8 = {
358
372
  const tagName = getElementType(node);
359
373
  if (!isFormInput(tagName)) return;
360
374
  const inputType = getAttributeValue(node, "type");
361
- if (inputType === "hidden") return;
362
- const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id");
375
+ if (inputType === "hidden" || inputType === "submit" || inputType === "reset") return;
376
+ const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id") || inputType === "button" && hasAttribute(node, "value") || inputType === "image" && hasAttribute(node, "alt");
363
377
  if (!hasAccessibleLabel) {
364
378
  context.report({
365
379
  node: astNode,
@@ -379,7 +393,7 @@ var rule9 = {
379
393
  type: "problem",
380
394
  docs: {
381
395
  description: 'Enforce that radio buttons are inside a fieldset or role="radiogroup".',
382
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/radio-group-requires-grouping.md"
396
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping"
383
397
  },
384
398
  messages: {
385
399
  missingGrouping: 'Radio buttons must be grouped inside a <fieldset> with <legend> or an element with role="radiogroup" and aria-label. Without grouping, screen readers announce each radio button independently with no indication they belong to a set.'
@@ -415,7 +429,7 @@ var rule10 = {
415
429
  type: "problem",
416
430
  docs: {
417
431
  description: "Enforce that tabIndex is not greater than 0.",
418
- url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/no-positive-tabindex.md"
432
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex"
419
433
  },
420
434
  messages: {
421
435
  positiveTabindex: "tabIndex must not be greater than 0 (found tabIndex={{ value }}). Positive tabIndex values break the natural tab order, creating unpredictable keyboard navigation. Use tabIndex={0} to make an element focusable in DOM order, or tabIndex={-1} for programmatic focus only."
@@ -458,7 +472,7 @@ var recommendedRules = Object.fromEntries(
458
472
  var plugin = {
459
473
  meta: {
460
474
  name: "eslint-plugin-a11y-enforce",
461
- version: "0.1.0"
475
+ version: "0.2.0"
462
476
  },
463
477
  rules,
464
478
  configs: {}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/ast-helpers.ts","../src/rules/dialog-requires-modal.ts","../src/rules/haspopup-role-match.ts","../src/rules/tooltip-no-interactive.ts","../src/rules/accordion-trigger-heading.ts","../src/rules/menuitem-not-button.ts","../src/rules/dialog-requires-title.ts","../src/rules/focusable-has-interaction.ts","../src/rules/input-requires-label.ts","../src/rules/radio-group-requires-grouping.ts","../src/rules/no-positive-tabindex.ts","../src/index.ts"],"sourcesContent":["/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'a', 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'string'\n ) {\n return attr.value.expression.value;\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr || attr.value === null) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/** Native HTML elements with built-in keyboard behavior. */\nexport function isInteractiveElement(tagName: string): boolean {\n return INTERACTIVE_ELEMENTS.has(tagName.toLowerCase());\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // Cast through `unknown` because JSXElement's readonly properties don't\n // overlap with Record's index signature under exactOptionalPropertyTypes.\n let current = node.parent as unknown as Record<string, unknown> | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement as unknown as JSXOpeningElement)) return true;\n }\n current = (current.parent as unknown as Record<string, unknown> | null) ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-modal.md',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/haspopup-role-match.md',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/tooltip-no-interactive.md',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a, input, select, textarea\n if (isInteractiveElement(tagName)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> with aria-expanded (the accordion trigger pattern)\n * should be wrapped in a heading element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/accordion-trigger-heading.md',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Only applies to <button> elements with aria-expanded.\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n if (tagName.toLowerCase() !== 'button') return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/menuitem-not-button.md',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-title.md',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/focusable-has-interaction.md',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, etc.) already have\n // built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Hidden inputs (type=\"hidden\") are excluded because they have no\n * visual or accessible representation.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/input-requires-label.md',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n // Hidden inputs have no visual or accessible representation\n const inputType = getAttributeValue(node, 'type');\n if (inputType === 'hidden') return;\n\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id');\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/radio-group-requires-grouping.md',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/no-positive-tabindex.md',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.1.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
1
+ {"version":3,"sources":["../src/utils/ast-helpers.ts","../src/rules/dialog-requires-modal.ts","../src/rules/haspopup-role-match.ts","../src/rules/tooltip-no-interactive.ts","../src/rules/accordion-trigger-heading.ts","../src/rules/menuitem-not-button.ts","../src/rules/dialog-requires-title.ts","../src/rules/focusable-has-interaction.ts","../src/rules/input-requires-label.ts","../src/rules/radio-group-requires-grouping.ts","../src/rules/no-positive-tabindex.ts","../src/index.ts"],"sourcesContent":["/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute, ASTParentNode } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal'\n ) {\n // String expression: aria-haspopup={\"menu\"}\n if (typeof attr.value.expression.value === 'string') {\n return attr.value.expression.value;\n }\n // Boolean expression: aria-modal={true}\n // JSX boolean expressions produce Literal with boolean value.\n // Coerce to string so callers can compare against \"true\"/\"false\"\n // consistently regardless of whether the author wrote\n // aria-modal=\"true\" or aria-modal={true}.\n if (typeof attr.value.expression.value === 'boolean') {\n return String(attr.value.expression.value);\n }\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr?.value) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n // UnaryExpression: tabIndex={-1} parses as { operator: '-', argument: Literal(1) }\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'UnaryExpression' &&\n attr.value.expression.operator === '-' &&\n attr.value.expression.argument?.type === 'Literal' &&\n typeof attr.value.expression.argument.value === 'number'\n ) {\n return -attr.value.expression.argument.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = Number.parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/**\n * Native HTML elements with built-in keyboard behavior.\n *\n * <a> is only interactive when it has an href attribute. Without href,\n * it's a placeholder anchor with no keyboard behavior and no implicit\n * role. This matters for tooltip-no-interactive (an <a> without href\n * inside a tooltip is not a violation) and focusable-has-interaction\n * (an <a> without href and tabIndex={0} SHOULD fire).\n */\nexport function isInteractiveElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n const lower = tagName.toLowerCase();\n if (INTERACTIVE_ELEMENTS.has(lower)) return true;\n if (lower === 'a') {\n // If no node provided, assume interactive (conservative)\n if (!node) return true;\n return hasAttribute(node, 'href');\n }\n return false;\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // ASTParentNode types the minimal shape we need from the parent chain.\n let current: ASTParentNode | null | undefined =\n node.parent as unknown as ASTParentNode | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement)) return true;\n }\n current = current.parent ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a (with href), input, select, textarea\n if (isInteractiveElement(tagName, node)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> or element with role=\"button\" that has aria-expanded\n * (the accordion trigger pattern) should be wrapped in a heading\n * element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Applies to <button> elements and elements with role=\"button\"\n // that have aria-expanded (the accordion trigger pattern).\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n const isButton =\n tagName.toLowerCase() === 'button' ||\n getAttributeValue(node, 'role') === 'button';\n\n if (!isButton) return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, a with href) already\n // have built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName, node)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Excluded input types:\n * - type=\"hidden\": no visual or accessible representation.\n * - type=\"submit\": browser provides default accessible name (\"Submit\").\n * - type=\"reset\": browser provides default accessible name (\"Reset\").\n *\n * Special input types:\n * - type=\"button\": accessible name comes from `value` attribute.\n * - type=\"image\": accessible name comes from `alt` attribute.\n *\n * These types derive their name differently than text inputs and\n * must not trigger false positives when labeled correctly via\n * their native mechanism.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n const inputType = getAttributeValue(node, 'type');\n\n // These types have browser-provided default accessible names\n // and do not require explicit labeling.\n if (inputType === 'hidden' || inputType === 'submit' || inputType === 'reset') return;\n\n // type=\"button\" gets its name from the value attribute.\n // type=\"image\" gets its name from the alt attribute.\n // Both also accept aria-label, aria-labelledby, and id.\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id') ||\n (inputType === 'button' && hasAttribute(node, 'value')) ||\n (inputType === 'image' && hasAttribute(node, 'alt'));\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.2.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-a11y-enforce",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "ESLint plugin that catches accessibility composition errors that element-level tools miss.",
5
5
  "author": "Venkatesh Mukundan <vmvenkatesh78@gmail.com>",
6
6
  "license": "MIT",
@@ -8,6 +8,8 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce"
10
10
  },
11
+ "homepage": "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce#readme",
12
+ "bugs": "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/issues",
11
13
  "keywords": [
12
14
  "eslint",
13
15
  "eslintplugin",
@@ -33,18 +35,9 @@
33
35
  "files": [
34
36
  "dist",
35
37
  "README.md",
38
+ "CHANGELOG.md",
36
39
  "LICENSE"
37
40
  ],
38
- "scripts": {
39
- "build": "tsup",
40
- "dev": "tsup --watch",
41
- "test": "vitest run",
42
- "test:watch": "vitest",
43
- "typecheck": "tsc --noEmit",
44
- "lint": "eslint src/",
45
- "clean": "rm -rf dist",
46
- "prepublishOnly": "pnpm clean && pnpm build"
47
- },
48
41
  "peerDependencies": {
49
42
  "eslint": ">=8.0.0"
50
43
  },
@@ -60,5 +53,13 @@
60
53
  "engines": {
61
54
  "node": ">=18.0.0"
62
55
  },
63
- "packageManager": "pnpm@9.15.0"
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
+ }