eslint-plugin-a11y-enforce 0.2.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/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
@@ -71,19 +71,27 @@ function getAttributeValue(node, attrName) {
71
71
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
72
72
  return attr.value.value;
73
73
  }
74
- if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "string") {
75
- 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
+ }
76
81
  }
77
82
  return void 0;
78
83
  }
79
84
  function getNumericValue(node, attrName) {
80
85
  const attr = findAttribute(node, attrName);
81
- if (!attr || attr.value === null) return void 0;
86
+ if (!attr?.value) return void 0;
82
87
  if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "number") {
83
88
  return attr.value.expression.value;
84
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
+ }
85
93
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
86
- const parsed = parseInt(attr.value.value, 10);
94
+ const parsed = Number.parseInt(attr.value.value, 10);
87
95
  if (!Number.isNaN(parsed)) return parsed;
88
96
  }
89
97
  return void 0;
@@ -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.2.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n","/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute, ASTParentNode } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'string'\n ) {\n return attr.value.expression.value;\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr || attr.value === null) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/**\n * Native HTML elements with built-in keyboard behavior.\n *\n * <a> is only interactive when it has an href attribute. Without href,\n * it's a placeholder anchor with no keyboard behavior and no implicit\n * role. This matters for tooltip-no-interactive (an <a> without href\n * inside a tooltip is not a violation) and focusable-has-interaction\n * (an <a> without href and tabIndex={0} SHOULD fire).\n */\nexport function isInteractiveElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n const lower = tagName.toLowerCase();\n if (INTERACTIVE_ELEMENTS.has(lower)) return true;\n if (lower === 'a') {\n // If no node provided, assume interactive (conservative)\n if (!node) return true;\n return hasAttribute(node, 'href');\n }\n return false;\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // ASTParentNode types the minimal shape we need from the parent chain.\n let current: ASTParentNode | null | undefined =\n node.parent as unknown as ASTParentNode | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement)) return true;\n }\n current = current.parent ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a (with href), input, select, textarea\n if (isInteractiveElement(tagName, node)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> or element with role=\"button\" that has aria-expanded\n * (the accordion trigger pattern) should be wrapped in a heading\n * element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Applies to <button> elements and elements with role=\"button\"\n // that have aria-expanded (the accordion trigger pattern).\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n const isButton =\n tagName.toLowerCase() === 'button' ||\n getAttributeValue(node, 'role') === 'button';\n\n if (!isButton) return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, a with href) already\n // have built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName, node)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Excluded input types:\n * - type=\"hidden\": no visual or accessible representation.\n * - type=\"submit\": browser provides default accessible name (\"Submit\").\n * - type=\"reset\": browser provides default accessible name (\"Reset\").\n *\n * Special input types:\n * - type=\"button\": accessible name comes from `value` attribute.\n * - type=\"image\": accessible name comes from `alt` attribute.\n *\n * These types derive their name differently than text inputs and\n * must not trigger false positives when labeled correctly via\n * their native mechanism.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n const inputType = getAttributeValue(node, 'type');\n\n // These types have browser-provided default accessible names\n // and do not require explicit labeling.\n if (inputType === 'hidden' || inputType === 'submit' || inputType === 'reset') return;\n\n // type=\"button\" gets its name from the value attribute.\n // type=\"image\" gets its name from the alt attribute.\n // Both also accept aria-label, aria-labelledby, and id.\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id') ||\n (inputType === 'button' && hasAttribute(node, 'value')) ||\n (inputType === 'image' && hasAttribute(node, 'alt'));\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
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
@@ -43,19 +43,27 @@ function getAttributeValue(node, attrName) {
43
43
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
44
44
  return attr.value.value;
45
45
  }
46
- if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "string") {
47
- 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
+ }
48
53
  }
49
54
  return void 0;
50
55
  }
51
56
  function getNumericValue(node, attrName) {
52
57
  const attr = findAttribute(node, attrName);
53
- if (!attr || attr.value === null) return void 0;
58
+ if (!attr?.value) return void 0;
54
59
  if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "number") {
55
60
  return attr.value.expression.value;
56
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
+ }
57
65
  if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
58
- const parsed = parseInt(attr.value.value, 10);
66
+ const parsed = Number.parseInt(attr.value.value, 10);
59
67
  if (!Number.isNaN(parsed)) return parsed;
60
68
  }
61
69
  return void 0;
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, ASTParentNode } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'string'\n ) {\n return attr.value.expression.value;\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr || attr.value === null) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/**\n * Native HTML elements with built-in keyboard behavior.\n *\n * <a> is only interactive when it has an href attribute. Without href,\n * it's a placeholder anchor with no keyboard behavior and no implicit\n * role. This matters for tooltip-no-interactive (an <a> without href\n * inside a tooltip is not a violation) and focusable-has-interaction\n * (an <a> without href and tabIndex={0} SHOULD fire).\n */\nexport function isInteractiveElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n const lower = tagName.toLowerCase();\n if (INTERACTIVE_ELEMENTS.has(lower)) return true;\n if (lower === 'a') {\n // If no node provided, assume interactive (conservative)\n if (!node) return true;\n return hasAttribute(node, 'href');\n }\n return false;\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // ASTParentNode types the minimal shape we need from the parent chain.\n let current: ASTParentNode | null | undefined =\n node.parent as unknown as ASTParentNode | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement)) return true;\n }\n current = current.parent ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-modal',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#haspopup-role-match',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#tooltip-no-interactive',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a (with href), input, select, textarea\n if (isInteractiveElement(tagName, node)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> or element with role=\"button\" that has aria-expanded\n * (the accordion trigger pattern) should be wrapped in a heading\n * element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#accordion-trigger-heading',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Applies to <button> elements and elements with role=\"button\"\n // that have aria-expanded (the accordion trigger pattern).\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n const isButton =\n tagName.toLowerCase() === 'button' ||\n getAttributeValue(node, 'role') === 'button';\n\n if (!isButton) return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#menuitem-not-button',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#dialog-requires-title',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#focusable-has-interaction',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, a with href) already\n // have built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName, node)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Excluded input types:\n * - type=\"hidden\": no visual or accessible representation.\n * - type=\"submit\": browser provides default accessible name (\"Submit\").\n * - type=\"reset\": browser provides default accessible name (\"Reset\").\n *\n * Special input types:\n * - type=\"button\": accessible name comes from `value` attribute.\n * - type=\"image\": accessible name comes from `alt` attribute.\n *\n * These types derive their name differently than text inputs and\n * must not trigger false positives when labeled correctly via\n * their native mechanism.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#input-requires-label',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n const inputType = getAttributeValue(node, 'type');\n\n // These types have browser-provided default accessible names\n // and do not require explicit labeling.\n if (inputType === 'hidden' || inputType === 'submit' || inputType === 'reset') return;\n\n // type=\"button\" gets its name from the value attribute.\n // type=\"image\" gets its name from the alt attribute.\n // Both also accept aria-label, aria-labelledby, and id.\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id') ||\n (inputType === 'button' && hasAttribute(node, 'value')) ||\n (inputType === 'image' && hasAttribute(node, 'alt'));\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#radio-group-requires-grouping',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/README.md#no-positive-tabindex',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.2.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n"],"mappings":"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","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
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.2.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",