eslint-plugin-a11y-enforce 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,485 @@
1
+ // src/utils/ast-helpers.ts
2
+ var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([
3
+ "a",
4
+ "button",
5
+ "input",
6
+ "select",
7
+ "textarea"
8
+ ]);
9
+ var HEADING_ELEMENTS = /* @__PURE__ */ new Set([
10
+ "h1",
11
+ "h2",
12
+ "h3",
13
+ "h4",
14
+ "h5",
15
+ "h6"
16
+ ]);
17
+ var FORM_INPUT_ELEMENTS = /* @__PURE__ */ new Set([
18
+ "input",
19
+ "select",
20
+ "textarea"
21
+ ]);
22
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
23
+ "button",
24
+ "link",
25
+ "textbox",
26
+ "checkbox",
27
+ "radio",
28
+ "combobox",
29
+ "menuitem",
30
+ "tab"
31
+ ]);
32
+ function findAttribute(node, attrName) {
33
+ for (const attr of node.attributes) {
34
+ if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier" && attr.name.name === attrName) {
35
+ return attr;
36
+ }
37
+ }
38
+ return void 0;
39
+ }
40
+ function getAttributeValue(node, attrName) {
41
+ const attr = findAttribute(node, attrName);
42
+ if (!attr) return void 0;
43
+ if (attr.value === null) return "true";
44
+ if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
45
+ return attr.value.value;
46
+ }
47
+ if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "string") {
48
+ return attr.value.expression.value;
49
+ }
50
+ return void 0;
51
+ }
52
+ function getNumericValue(node, attrName) {
53
+ const attr = findAttribute(node, attrName);
54
+ if (!attr || attr.value === null) return void 0;
55
+ if (attr.value.type === "JSXExpressionContainer" && attr.value.expression.type === "Literal" && typeof attr.value.expression.value === "number") {
56
+ return attr.value.expression.value;
57
+ }
58
+ if (attr.value.type === "Literal" && typeof attr.value.value === "string") {
59
+ const parsed = parseInt(attr.value.value, 10);
60
+ if (!Number.isNaN(parsed)) return parsed;
61
+ }
62
+ return void 0;
63
+ }
64
+ function hasAttribute(node, attrName) {
65
+ return findAttribute(node, attrName) !== void 0;
66
+ }
67
+ function hasAnyEventHandler(node, handlerNames) {
68
+ return handlerNames.some((name) => hasAttribute(node, name));
69
+ }
70
+ function getElementType(node) {
71
+ if (node.name.type === "JSXIdentifier" && "name" in node.name) {
72
+ return node.name.name ?? "";
73
+ }
74
+ return "";
75
+ }
76
+ function isInteractiveElement(tagName) {
77
+ return INTERACTIVE_ELEMENTS.has(tagName.toLowerCase());
78
+ }
79
+ function isHeadingElement(tagName, node) {
80
+ if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;
81
+ if (node && getAttributeValue(node, "role") === "heading") return true;
82
+ return false;
83
+ }
84
+ function isFormInput(tagName) {
85
+ return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());
86
+ }
87
+ function hasMatchingAncestor(node, predicate) {
88
+ let current = node.parent;
89
+ while (current) {
90
+ if (current.type === "JSXElement" && current.openingElement) {
91
+ if (predicate(current.openingElement)) return true;
92
+ }
93
+ current = current.parent ?? null;
94
+ }
95
+ return false;
96
+ }
97
+
98
+ // src/rules/dialog-requires-modal.ts
99
+ var DIALOG_ROLES = ["dialog", "alertdialog"];
100
+ var rule = {
101
+ meta: {
102
+ type: "problem",
103
+ docs: {
104
+ description: 'Enforce that elements with role="dialog" have aria-modal="true".',
105
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-modal.md"
106
+ },
107
+ messages: {
108
+ missingAriaModal: 'Elements with role="{{ role }}" must have aria-modal="true". Without aria-modal, screen readers will not restrict navigation to the dialog content, allowing users to accidentally interact with the page behind it. Add aria-modal="true" to this element.'
109
+ },
110
+ schema: []
111
+ },
112
+ create(context) {
113
+ return {
114
+ JSXOpeningElement(astNode) {
115
+ const node = astNode;
116
+ const role = getAttributeValue(node, "role");
117
+ if (!role || !DIALOG_ROLES.includes(role)) return;
118
+ const ariaModal = getAttributeValue(node, "aria-modal");
119
+ if (ariaModal !== "true") {
120
+ context.report({ node: astNode, messageId: "missingAriaModal", data: { role } });
121
+ }
122
+ }
123
+ };
124
+ }
125
+ };
126
+ var dialog_requires_modal_default = rule;
127
+
128
+ // src/rules/haspopup-role-match.ts
129
+ var VALID_HASPOPUP_VALUES = /* @__PURE__ */ new Set([
130
+ "menu",
131
+ "listbox",
132
+ "tree",
133
+ "grid",
134
+ "dialog",
135
+ "true",
136
+ "false"
137
+ ]);
138
+ var rule2 = {
139
+ meta: {
140
+ type: "problem",
141
+ docs: {
142
+ description: "Enforce that aria-haspopup has a valid ARIA value.",
143
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/haspopup-role-match.md"
144
+ },
145
+ messages: {
146
+ invalidHaspopup: 'aria-haspopup value "{{ value }}" is not valid. Allowed values are: menu, listbox, tree, grid, dialog, true, false. The value must match the role of the popup content it triggers. Screen readers use this value to announce the type of popup that will appear.'
147
+ },
148
+ schema: []
149
+ },
150
+ create(context) {
151
+ return {
152
+ JSXOpeningElement(astNode) {
153
+ const node = astNode;
154
+ const haspopup = getAttributeValue(node, "aria-haspopup");
155
+ if (haspopup === void 0) return;
156
+ if (!VALID_HASPOPUP_VALUES.has(haspopup)) {
157
+ context.report({ node: astNode, messageId: "invalidHaspopup", data: { value: haspopup } });
158
+ }
159
+ }
160
+ };
161
+ }
162
+ };
163
+ var haspopup_role_match_default = rule2;
164
+
165
+ // src/rules/tooltip-no-interactive.ts
166
+ var rule3 = {
167
+ meta: {
168
+ type: "problem",
169
+ docs: {
170
+ description: "Enforce that tooltip content does not contain interactive elements.",
171
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/tooltip-no-interactive.md"
172
+ },
173
+ messages: {
174
+ interactiveInTooltip: 'Tooltip (role="tooltip") must not contain interactive elements. Tooltips are non-interactive by design. Users cannot Tab to content inside a tooltip because it disappears on blur. If you need interactive content in a popup, use a Popover or Dialog instead.'
175
+ },
176
+ schema: []
177
+ },
178
+ create(context) {
179
+ function isInsideTooltip(node) {
180
+ return hasMatchingAncestor(
181
+ node,
182
+ (ancestor) => getAttributeValue(ancestor, "role") === "tooltip"
183
+ );
184
+ }
185
+ return {
186
+ JSXOpeningElement(astNode) {
187
+ const node = astNode;
188
+ if (getAttributeValue(node, "role") === "tooltip") return;
189
+ if (!isInsideTooltip(node)) return;
190
+ const tagName = getElementType(node);
191
+ if (isInteractiveElement(tagName)) {
192
+ context.report({ node: astNode, messageId: "interactiveInTooltip" });
193
+ return;
194
+ }
195
+ const tabIndex = getNumericValue(node, "tabIndex");
196
+ if (tabIndex !== void 0 && tabIndex >= 0) {
197
+ context.report({ node: astNode, messageId: "interactiveInTooltip" });
198
+ return;
199
+ }
200
+ const childRole = getAttributeValue(node, "role");
201
+ if (childRole && INTERACTIVE_ROLES.has(childRole)) {
202
+ context.report({ node: astNode, messageId: "interactiveInTooltip" });
203
+ }
204
+ }
205
+ };
206
+ }
207
+ };
208
+ var tooltip_no_interactive_default = rule3;
209
+
210
+ // src/rules/accordion-trigger-heading.ts
211
+ var rule4 = {
212
+ meta: {
213
+ type: "problem",
214
+ docs: {
215
+ description: "Enforce that accordion trigger buttons are inside heading elements.",
216
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/accordion-trigger-heading.md"
217
+ },
218
+ messages: {
219
+ missingHeading: 'Accordion trigger (button with aria-expanded) should be inside a heading element (h1-h6) or an element with role="heading". Without a heading, screen reader users navigating by headings will not discover this accordion section. Wrap the button in an appropriate heading element.'
220
+ },
221
+ schema: []
222
+ },
223
+ create(context) {
224
+ return {
225
+ JSXOpeningElement(astNode) {
226
+ const node = astNode;
227
+ const tagName = getElementType(node);
228
+ if (tagName.toLowerCase() !== "button") return;
229
+ if (!hasAttribute(node, "aria-expanded")) return;
230
+ const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {
231
+ const ancestorTag = getElementType(ancestor);
232
+ return isHeadingElement(ancestorTag, ancestor);
233
+ });
234
+ if (!hasHeadingAncestor) {
235
+ context.report({ node: astNode, messageId: "missingHeading" });
236
+ }
237
+ }
238
+ };
239
+ }
240
+ };
241
+ var accordion_trigger_heading_default = rule4;
242
+
243
+ // src/rules/menuitem-not-button.ts
244
+ var MENUITEM_ROLES = [
245
+ "menuitem",
246
+ "menuitemcheckbox",
247
+ "menuitemradio"
248
+ ];
249
+ var rule5 = {
250
+ meta: {
251
+ type: "problem",
252
+ docs: {
253
+ description: 'Enforce that role="menuitem" is not used on button elements.',
254
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/menuitem-not-button.md"
255
+ },
256
+ messages: {
257
+ menuitemOnButton: 'role="{{ role }}" should not be used on <button> elements. Buttons have an implicit "button" role, which causes some screen readers to double-announce: "button, menuitem." Use a <div> or <li> with role="{{ role }}" and tabIndex={-1} instead.'
258
+ },
259
+ schema: []
260
+ },
261
+ create(context) {
262
+ return {
263
+ JSXOpeningElement(astNode) {
264
+ const node = astNode;
265
+ const role = getAttributeValue(node, "role");
266
+ if (!role || !MENUITEM_ROLES.includes(role)) return;
267
+ const tagName = getElementType(node);
268
+ if (tagName.toLowerCase() !== "button") return;
269
+ context.report({ node: astNode, messageId: "menuitemOnButton", data: { role } });
270
+ }
271
+ };
272
+ }
273
+ };
274
+ var menuitem_not_button_default = rule5;
275
+
276
+ // src/rules/dialog-requires-title.ts
277
+ var DIALOG_ROLES2 = ["dialog", "alertdialog"];
278
+ var rule6 = {
279
+ meta: {
280
+ type: "problem",
281
+ docs: {
282
+ description: "Enforce that dialogs have an accessible name via aria-labelledby or aria-label.",
283
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-title.md"
284
+ },
285
+ messages: {
286
+ missingDialogTitle: 'Dialog (role="{{ role }}") must have an accessible name via aria-labelledby or aria-label. Without a name, screen readers announce "dialog" with no context. Add aria-labelledby pointing to a heading inside the dialog, or aria-label with a descriptive name.'
287
+ },
288
+ schema: []
289
+ },
290
+ create(context) {
291
+ return {
292
+ JSXOpeningElement(astNode) {
293
+ const node = astNode;
294
+ const role = getAttributeValue(node, "role");
295
+ if (!role || !DIALOG_ROLES2.includes(role)) return;
296
+ const hasAccessibleName = hasAttribute(node, "aria-labelledby") || hasAttribute(node, "aria-label");
297
+ if (!hasAccessibleName) {
298
+ context.report({ node: astNode, messageId: "missingDialogTitle", data: { role } });
299
+ }
300
+ }
301
+ };
302
+ }
303
+ };
304
+ var dialog_requires_title_default = rule6;
305
+
306
+ // src/rules/focusable-has-interaction.ts
307
+ var KEYBOARD_HANDLERS = [
308
+ "onKeyDown",
309
+ "onKeyUp",
310
+ "onKeyPress"
311
+ ];
312
+ var rule7 = {
313
+ meta: {
314
+ type: "problem",
315
+ docs: {
316
+ description: "Enforce that elements with tabIndex={0} have keyboard event handlers.",
317
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/focusable-has-interaction.md"
318
+ },
319
+ messages: {
320
+ missingKeyboardHandler: "Element with tabIndex={0} is focusable but has no keyboard event handler (onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this element but cannot interact with it. Add an onKeyDown handler, or remove tabIndex if the element is not meant to be interactive."
321
+ },
322
+ schema: []
323
+ },
324
+ create(context) {
325
+ return {
326
+ JSXOpeningElement(astNode) {
327
+ const node = astNode;
328
+ const tabIndex = getNumericValue(node, "tabIndex");
329
+ if (tabIndex !== 0) return;
330
+ const tagName = getElementType(node);
331
+ if (isInteractiveElement(tagName)) return;
332
+ if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {
333
+ context.report({ node: astNode, messageId: "missingKeyboardHandler" });
334
+ }
335
+ }
336
+ };
337
+ }
338
+ };
339
+ var focusable_has_interaction_default = rule7;
340
+
341
+ // src/rules/input-requires-label.ts
342
+ var rule8 = {
343
+ meta: {
344
+ type: "problem",
345
+ docs: {
346
+ description: "Enforce that form inputs have an accessible label.",
347
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/input-requires-label.md"
348
+ },
349
+ messages: {
350
+ missingLabel: 'Form input ({{ element }}) must have an accessible label. Screen readers announce inputs by their label. Without one, users hear "edit text" with no context. Add aria-label, aria-labelledby, or associate a <label> element using htmlFor. Note: placeholder is not a substitute for a label.'
351
+ },
352
+ schema: []
353
+ },
354
+ create(context) {
355
+ return {
356
+ JSXOpeningElement(astNode) {
357
+ const node = astNode;
358
+ const tagName = getElementType(node);
359
+ if (!isFormInput(tagName)) return;
360
+ const inputType = getAttributeValue(node, "type");
361
+ if (inputType === "hidden") return;
362
+ const hasAccessibleLabel = hasAttribute(node, "aria-label") || hasAttribute(node, "aria-labelledby") || hasAttribute(node, "id");
363
+ if (!hasAccessibleLabel) {
364
+ context.report({
365
+ node: astNode,
366
+ messageId: "missingLabel",
367
+ data: { element: `<${tagName}>` }
368
+ });
369
+ }
370
+ }
371
+ };
372
+ }
373
+ };
374
+ var input_requires_label_default = rule8;
375
+
376
+ // src/rules/radio-group-requires-grouping.ts
377
+ var rule9 = {
378
+ meta: {
379
+ type: "problem",
380
+ docs: {
381
+ description: 'Enforce that radio buttons are inside a fieldset or role="radiogroup".',
382
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/radio-group-requires-grouping.md"
383
+ },
384
+ messages: {
385
+ missingGrouping: 'Radio buttons must be grouped inside a <fieldset> with <legend> or an element with role="radiogroup" and aria-label. Without grouping, screen readers announce each radio button independently with no indication they belong to a set.'
386
+ },
387
+ schema: []
388
+ },
389
+ create(context) {
390
+ return {
391
+ JSXOpeningElement(astNode) {
392
+ const node = astNode;
393
+ const tagName = getElementType(node);
394
+ if (tagName.toLowerCase() !== "input") return;
395
+ const inputType = getAttributeValue(node, "type");
396
+ if (inputType !== "radio") return;
397
+ const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {
398
+ const ancestorTag = getElementType(ancestor);
399
+ if (ancestorTag.toLowerCase() === "fieldset") return true;
400
+ if (getAttributeValue(ancestor, "role") === "radiogroup") return true;
401
+ return false;
402
+ });
403
+ if (!hasGroupingAncestor) {
404
+ context.report({ node: astNode, messageId: "missingGrouping" });
405
+ }
406
+ }
407
+ };
408
+ }
409
+ };
410
+ var radio_group_requires_grouping_default = rule9;
411
+
412
+ // src/rules/no-positive-tabindex.ts
413
+ var rule10 = {
414
+ meta: {
415
+ type: "problem",
416
+ docs: {
417
+ description: "Enforce that tabIndex is not greater than 0.",
418
+ url: "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/no-positive-tabindex.md"
419
+ },
420
+ messages: {
421
+ positiveTabindex: "tabIndex must not be greater than 0 (found tabIndex={{ value }}). Positive tabIndex values break the natural tab order, creating unpredictable keyboard navigation. Use tabIndex={0} to make an element focusable in DOM order, or tabIndex={-1} for programmatic focus only."
422
+ },
423
+ schema: []
424
+ },
425
+ create(context) {
426
+ return {
427
+ JSXOpeningElement(astNode) {
428
+ const node = astNode;
429
+ const tabIndex = getNumericValue(node, "tabIndex");
430
+ if (tabIndex === void 0 || tabIndex <= 0) return;
431
+ context.report({
432
+ node: astNode,
433
+ messageId: "positiveTabindex",
434
+ data: { value: String(tabIndex) }
435
+ });
436
+ }
437
+ };
438
+ }
439
+ };
440
+ var no_positive_tabindex_default = rule10;
441
+
442
+ // src/index.ts
443
+ var rules = {
444
+ "dialog-requires-modal": dialog_requires_modal_default,
445
+ "haspopup-role-match": haspopup_role_match_default,
446
+ "tooltip-no-interactive": tooltip_no_interactive_default,
447
+ "accordion-trigger-heading": accordion_trigger_heading_default,
448
+ "menuitem-not-button": menuitem_not_button_default,
449
+ "dialog-requires-title": dialog_requires_title_default,
450
+ "focusable-has-interaction": focusable_has_interaction_default,
451
+ "input-requires-label": input_requires_label_default,
452
+ "radio-group-requires-grouping": radio_group_requires_grouping_default,
453
+ "no-positive-tabindex": no_positive_tabindex_default
454
+ };
455
+ var recommendedRules = Object.fromEntries(
456
+ Object.keys(rules).map((name) => [`a11y-enforce/${name}`, "error"])
457
+ );
458
+ var plugin = {
459
+ meta: {
460
+ name: "eslint-plugin-a11y-enforce",
461
+ version: "0.1.0"
462
+ },
463
+ rules,
464
+ configs: {}
465
+ };
466
+ var flatRecommended = {
467
+ plugins: { "a11y-enforce": plugin },
468
+ rules: recommendedRules
469
+ };
470
+ var legacyRecommended = {
471
+ plugins: ["a11y-enforce"],
472
+ rules: recommendedRules
473
+ };
474
+ plugin.configs = {
475
+ recommended: flatRecommended,
476
+ "flat/recommended": flatRecommended,
477
+ "legacy/recommended": legacyRecommended
478
+ };
479
+ var index_default = plugin;
480
+ export {
481
+ index_default as default,
482
+ plugin,
483
+ rules
484
+ };
485
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/ast-helpers.ts","../src/rules/dialog-requires-modal.ts","../src/rules/haspopup-role-match.ts","../src/rules/tooltip-no-interactive.ts","../src/rules/accordion-trigger-heading.ts","../src/rules/menuitem-not-button.ts","../src/rules/dialog-requires-title.ts","../src/rules/focusable-has-interaction.ts","../src/rules/input-requires-label.ts","../src/rules/radio-group-requires-grouping.ts","../src/rules/no-positive-tabindex.ts","../src/index.ts"],"sourcesContent":["/**\n * Shared AST utilities for JSX accessibility rule visitors.\n *\n * Handles the three value representations ESLint's parser produces:\n * 1. String literal: role=\"dialog\" -> Literal node\n * 2. Expression literal: tabIndex={0} -> JSXExpressionContainer > Literal\n * 3. Boolean shorthand: hidden -> null (present, no value)\n *\n * Dynamic expressions (tabIndex={someVar}) return undefined because\n * static analysis cannot resolve runtime values.\n */\n\nimport type { JSXOpeningElement, JSXAttribute } from '../types';\n\n// ── Element classification ───────────────────────────────────────────\n// Module-level constants prevent per-visit allocation.\n\nconst INTERACTIVE_ELEMENTS: ReadonlySet<string> = new Set([\n 'a', 'button', 'input', 'select', 'textarea',\n]);\n\nconst HEADING_ELEMENTS: ReadonlySet<string> = new Set([\n 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',\n]);\n\nconst FORM_INPUT_ELEMENTS: ReadonlySet<string> = new Set([\n 'input', 'select', 'textarea',\n]);\n\n/**\n * ARIA roles that make an element interactive.\n * Exported for use by tooltip-no-interactive.\n */\nexport const INTERACTIVE_ROLES: ReadonlySet<string> = new Set([\n 'button', 'link', 'textbox', 'checkbox',\n 'radio', 'combobox', 'menuitem', 'tab',\n]);\n\n// ── Attribute extraction ─────────────────────────────────────────────\n\n/**\n * Find a JSXAttribute by name. Returns undefined for absent\n * attributes and skips spread attributes.\n */\nfunction findAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): JSXAttribute | undefined {\n for (const attr of node.attributes) {\n if (\n attr.type === 'JSXAttribute' &&\n attr.name.type === 'JSXIdentifier' &&\n attr.name.name === attrName\n ) {\n return attr;\n }\n }\n return undefined;\n}\n\n/**\n * Extract a static string value from a JSX attribute.\n *\n * - `role=\"dialog\"` -> \"dialog\"\n * - `role={\"dialog\"}` -> \"dialog\"\n * - `<div hidden />` -> \"true\" (boolean shorthand)\n * - `role={variable}` -> undefined (dynamic)\n */\nexport function getAttributeValue(\n node: JSXOpeningElement,\n attrName: string,\n): string | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr) return undefined;\n\n if (attr.value === null) return 'true';\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n return attr.value.value;\n }\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'string'\n ) {\n return attr.value.expression.value;\n }\n\n return undefined;\n}\n\n/**\n * Extract a static numeric value from a JSX attribute.\n *\n * - `tabIndex={0}` -> 0\n * - `tabIndex=\"0\"` -> 0\n * - `tabIndex={x}` -> undefined (dynamic)\n */\nexport function getNumericValue(\n node: JSXOpeningElement,\n attrName: string,\n): number | undefined {\n const attr = findAttribute(node, attrName);\n if (!attr || attr.value === null) return undefined;\n\n if (\n attr.value.type === 'JSXExpressionContainer' &&\n attr.value.expression.type === 'Literal' &&\n typeof attr.value.expression.value === 'number'\n ) {\n return attr.value.expression.value;\n }\n\n if (attr.value.type === 'Literal' && typeof attr.value.value === 'string') {\n const parsed = parseInt(attr.value.value, 10);\n if (!Number.isNaN(parsed)) return parsed;\n }\n\n return undefined;\n}\n\n/** Check whether a JSX attribute exists on an element (ignores value). */\nexport function hasAttribute(\n node: JSXOpeningElement,\n attrName: string,\n): boolean {\n return findAttribute(node, attrName) !== undefined;\n}\n\n/** Check whether any of the listed event handler props exist. */\nexport function hasAnyEventHandler(\n node: JSXOpeningElement,\n handlerNames: ReadonlyArray<string>,\n): boolean {\n return handlerNames.some((name) => hasAttribute(node, name));\n}\n\n// ── Element type ─────────────────────────────────────────────────────\n\n/**\n * Get the tag name from a JSXOpeningElement.\n * Returns empty string for member expressions (<Foo.Bar />)\n * which our rules don't need to inspect.\n */\nexport function getElementType(node: JSXOpeningElement): string {\n if (node.name.type === 'JSXIdentifier' && 'name' in node.name) {\n return node.name.name ?? '';\n }\n return '';\n}\n\n// ── Element classification ───────────────────────────────────────────\n\n/** Native HTML elements with built-in keyboard behavior. */\nexport function isInteractiveElement(tagName: string): boolean {\n return INTERACTIVE_ELEMENTS.has(tagName.toLowerCase());\n}\n\n/**\n * Heading check: h1-h6 by tag name or role=\"heading\".\n * WAI-ARIA requires accordion triggers inside headings\n * for document structure navigation.\n */\nexport function isHeadingElement(\n tagName: string,\n node?: JSXOpeningElement,\n): boolean {\n if (HEADING_ELEMENTS.has(tagName.toLowerCase())) return true;\n if (node && getAttributeValue(node, 'role') === 'heading') return true;\n return false;\n}\n\n/** Form inputs that require accessible labels. */\nexport function isFormInput(tagName: string): boolean {\n return FORM_INPUT_ELEMENTS.has(tagName.toLowerCase());\n}\n\n// ── Ancestor traversal ───────────────────────────────────────────────\n\n/**\n * Walk up the JSX tree checking ancestors against a predicate.\n *\n * Used instead of mutable state flags (e.g. `insideTooltip = true`)\n * because ancestor walking is stateless and handles nested components,\n * conditional rendering, and interleaved elements correctly.\n */\nexport function hasMatchingAncestor(\n node: JSXOpeningElement,\n predicate: (ancestor: JSXOpeningElement) => boolean,\n): boolean {\n // ESLint's AST nodes have a `parent` property set during traversal.\n // We walk up until we hit the program root (parent is undefined/null).\n // Cast through `unknown` because JSXElement's readonly properties don't\n // overlap with Record's index signature under exactOptionalPropertyTypes.\n let current = node.parent as unknown as Record<string, unknown> | null;\n\n while (current) {\n if (current.type === 'JSXElement' && current.openingElement) {\n if (predicate(current.openingElement as unknown as JSXOpeningElement)) return true;\n }\n current = (current.parent as unknown as Record<string, unknown> | null) ?? null;\n }\n\n return false;\n}\n","/**\n * Rule: dialog-requires-modal\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must include\n * aria-modal=\"true\". Without it, screen readers allow virtual cursor\n * navigation outside the dialog, letting users interact with content\n * that should be blocked by the modal overlay.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with role=\"dialog\" have aria-modal=\"true\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-modal.md',\n },\n messages: {\n missingAriaModal:\n 'Elements with role=\"{{ role }}\" must have aria-modal=\"true\". ' +\n 'Without aria-modal, screen readers will not restrict navigation ' +\n 'to the dialog content, allowing users to accidentally interact ' +\n 'with the page behind it. Add aria-modal=\"true\" to this element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const ariaModal = getAttributeValue(node, 'aria-modal');\n if (ariaModal !== 'true') {\n context.report({ node: astNode, messageId: 'missingAriaModal', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: haspopup-role-match\n *\n * Validates that aria-haspopup uses a value from the ARIA spec's\n * allowed set: menu, listbox, tree, grid, dialog, true, false.\n *\n * Screen readers announce the popup type based on this value. An\n * invalid value (e.g. \"tooltip\", \"dropdown\") is silently treated\n * as \"false\" by user agents, which means the popup existence is\n * never announced at all.\n *\n * @see https://www.w3.org/TR/wai-aria-1.2/#aria-haspopup\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue } from '../utils/ast-helpers';\n\nconst VALID_HASPOPUP_VALUES: ReadonlySet<string> = new Set([\n 'menu', 'listbox', 'tree', 'grid', 'dialog', 'true', 'false',\n]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that aria-haspopup has a valid ARIA value.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/haspopup-role-match.md',\n },\n messages: {\n invalidHaspopup:\n 'aria-haspopup value \"{{ value }}\" is not valid. ' +\n 'Allowed values are: menu, listbox, tree, grid, dialog, true, false. ' +\n 'The value must match the role of the popup content it triggers. ' +\n 'Screen readers use this value to announce the type of popup that will appear.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const haspopup = getAttributeValue(node, 'aria-haspopup');\n\n if (haspopup === undefined) return;\n\n if (!VALID_HASPOPUP_VALUES.has(haspopup)) {\n context.report({ node: astNode, messageId: 'invalidHaspopup', data: { value: haspopup } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: tooltip-no-interactive\n *\n * Elements with role=\"tooltip\" must not contain focusable children\n * (buttons, links, inputs, or elements with tabIndex >= 0).\n *\n * Tooltips disappear on blur/mouse-leave. A keyboard user cannot Tab\n * into a tooltip to reach interactive content inside it. Sighted mouse\n * users can click buttons in tooltips, but keyboard and screen reader\n * users cannot, creating an inequitable experience.\n *\n * If interactive content is needed in a popup, use role=\"dialog\"\n * (Popover or Dialog) instead.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getAttributeValue,\n getElementType,\n getNumericValue,\n isInteractiveElement,\n hasMatchingAncestor,\n INTERACTIVE_ROLES,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tooltip content does not contain interactive elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/tooltip-no-interactive.md',\n },\n messages: {\n interactiveInTooltip:\n 'Tooltip (role=\"tooltip\") must not contain interactive elements. ' +\n 'Tooltips are non-interactive by design. Users cannot Tab to content ' +\n 'inside a tooltip because it disappears on blur. If you need ' +\n 'interactive content in a popup, use a Popover or Dialog instead.',\n },\n schema: [],\n },\n\n create(context) {\n /**\n * Check if the current node is nested inside a role=\"tooltip\" ancestor.\n * Uses stateless ancestor walking instead of a mutable boolean flag,\n * which would break with nested tooltips or interleaved elements.\n */\n function isInsideTooltip(node: JSXOpeningElement): boolean {\n return hasMatchingAncestor(\n node,\n (ancestor) => getAttributeValue(ancestor, 'role') === 'tooltip',\n );\n }\n\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n\n // Don't check the tooltip element itself, only its descendants\n if (getAttributeValue(node, 'role') === 'tooltip') return;\n if (!isInsideTooltip(node)) return;\n\n const tagName = getElementType(node);\n\n // Native interactive elements: button, a, input, select, textarea\n if (isInteractiveElement(tagName)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements made focusable via tabIndex >= 0\n const tabIndex = getNumericValue(node, 'tabIndex');\n if (tabIndex !== undefined && tabIndex >= 0) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n return;\n }\n\n // Elements with interactive ARIA roles\n const childRole = getAttributeValue(node, 'role');\n if (childRole && INTERACTIVE_ROLES.has(childRole)) {\n context.report({ node: astNode, messageId: 'interactiveInTooltip' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: accordion-trigger-heading\n *\n * A <button> with aria-expanded (the accordion trigger pattern)\n * should be wrapped in a heading element (h1-h6 or role=\"heading\").\n *\n * Screen reader users commonly navigate pages by headings (the H key\n * in NVDA/JAWS). Without a heading wrapper, accordion sections are\n * invisible to heading-based navigation and can only be found by\n * reading the entire page sequentially.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/accordion/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n hasAttribute,\n hasMatchingAncestor,\n isHeadingElement,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that accordion trigger buttons are inside heading elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/accordion-trigger-heading.md',\n },\n messages: {\n missingHeading:\n 'Accordion trigger (button with aria-expanded) should be inside a heading ' +\n 'element (h1-h6) or an element with role=\"heading\". Without a heading, ' +\n 'screen reader users navigating by headings will not discover this ' +\n 'accordion section. Wrap the button in an appropriate heading element.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n // Only applies to <button> elements with aria-expanded.\n // Other elements with aria-expanded (e.g. combobox triggers)\n // have different structural requirements.\n if (tagName.toLowerCase() !== 'button') return;\n if (!hasAttribute(node, 'aria-expanded')) return;\n\n const hasHeadingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n return isHeadingElement(ancestorTag, ancestor);\n });\n\n if (!hasHeadingAncestor) {\n context.report({ node: astNode, messageId: 'missingHeading' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: menuitem-not-button\n *\n * Elements with role=\"menuitem\", \"menuitemcheckbox\", or \"menuitemradio\"\n * should not be <button> elements.\n *\n * <button> has an implicit role of \"button\". Adding role=\"menuitem\"\n * overrides it at the ARIA level, but some screen readers (notably\n * NVDA with Firefox) announce both: \"button, menuitem, Edit.\"\n * This double announcement confuses users about the element's purpose.\n *\n * The correct pattern is a <div> or <li> with role=\"menuitem\" and\n * tabIndex={-1} for programmatic focus via roving tabindex.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getElementType, getAttributeValue } from '../utils/ast-helpers';\n\nconst MENUITEM_ROLES: ReadonlyArray<string> = [\n 'menuitem', 'menuitemcheckbox', 'menuitemradio',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that role=\"menuitem\" is not used on button elements.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/menuitem-not-button.md',\n },\n messages: {\n menuitemOnButton:\n 'role=\"{{ role }}\" should not be used on <button> elements. ' +\n 'Buttons have an implicit \"button\" role, which causes some screen ' +\n 'readers to double-announce: \"button, menuitem.\" Use a <div> or ' +\n '<li> with role=\"{{ role }}\" and tabIndex={-1} instead.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !MENUITEM_ROLES.includes(role)) return;\n\n const tagName = getElementType(node);\n if (tagName.toLowerCase() !== 'button') return;\n\n context.report({ node: astNode, messageId: 'menuitemOnButton', data: { role } });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: dialog-requires-title\n *\n * Elements with role=\"dialog\" or role=\"alertdialog\" must have an\n * accessible name via aria-labelledby or aria-label.\n *\n * Without a name, screen readers announce \"dialog\" with no context.\n * The user has no idea what the dialog is about until they read its\n * entire content. aria-labelledby pointing to a heading inside the\n * dialog gives an immediate announcement: \"Confirm deletion, dialog.\"\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getAttributeValue, hasAttribute } from '../utils/ast-helpers';\n\nconst DIALOG_ROLES: ReadonlyArray<string> = ['dialog', 'alertdialog'];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that dialogs have an accessible name via aria-labelledby or aria-label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/dialog-requires-title.md',\n },\n messages: {\n missingDialogTitle:\n 'Dialog (role=\"{{ role }}\") must have an accessible name via ' +\n 'aria-labelledby or aria-label. Without a name, screen readers ' +\n 'announce \"dialog\" with no context. Add aria-labelledby pointing ' +\n 'to a heading inside the dialog, or aria-label with a descriptive name.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const role = getAttributeValue(node, 'role');\n\n if (!role || !DIALOG_ROLES.includes(role)) return;\n\n const hasAccessibleName =\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'aria-label');\n\n if (!hasAccessibleName) {\n context.report({ node: astNode, messageId: 'missingDialogTitle', data: { role } });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: focusable-has-interaction\n *\n * Elements with tabIndex={0} must have at least one keyboard event\n * handler (onKeyDown, onKeyUp, or onKeyPress).\n *\n * tabIndex={0} places an element in the sequential focus order.\n * A keyboard user can Tab to it, which implies it's interactive.\n * If there's no keyboard handler, the element is a dead end in the\n * Tab sequence: reachable but inert.\n *\n * This differs from jsx-a11y's click-events-have-key-events, which\n * checks onClick. We check tabIndex directly, catching cases where\n * developers add tabIndex for \"focus styling\" without understanding\n * the keyboard interaction contract it creates.\n *\n * tabIndex={-1} is excluded: it makes an element programmatically\n * focusable (via .focus()) but does not add it to the Tab sequence.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getNumericValue,\n hasAnyEventHandler,\n isInteractiveElement,\n getElementType,\n} from '../utils/ast-helpers';\n\nconst KEYBOARD_HANDLERS: ReadonlyArray<string> = [\n 'onKeyDown', 'onKeyUp', 'onKeyPress',\n];\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that elements with tabIndex={0} have keyboard event handlers.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/focusable-has-interaction.md',\n },\n messages: {\n missingKeyboardHandler:\n 'Element with tabIndex={0} is focusable but has no keyboard event handler ' +\n '(onKeyDown, onKeyUp, or onKeyPress). Keyboard users can Tab to this ' +\n 'element but cannot interact with it. Add an onKeyDown handler, or ' +\n 'remove tabIndex if the element is not meant to be interactive.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n // Only check tabIndex={0}. Negative values are programmatic-only.\n if (tabIndex !== 0) return;\n\n // Native interactive elements (button, input, etc.) already have\n // built-in keyboard behavior. Adding tabIndex={0} to them is\n // redundant but not a violation.\n const tagName = getElementType(node);\n if (isInteractiveElement(tagName)) return;\n\n if (!hasAnyEventHandler(node, KEYBOARD_HANDLERS)) {\n context.report({ node: astNode, messageId: 'missingKeyboardHandler' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: input-requires-label\n *\n * <input>, <select>, and <textarea> elements must have an accessible\n * label via aria-label, aria-labelledby, or an id (which implies a\n * <label htmlFor> association may exist).\n *\n * Without a label, screen readers announce \"edit text\" or \"combobox\"\n * with no context. The user has to guess what to type. Placeholder\n * text is NOT a label: screen readers may not announce it, and it\n * disappears on input.\n *\n * The id check is intentionally lenient: if the input has an id, we\n * assume a label[htmlFor] exists somewhere. Cross-file label\n * association requires type-aware analysis that ESLint's per-file\n * visitor cannot do. False positives from an overly strict rule cause\n * developers to disable it entirely.\n *\n * Hidden inputs (type=\"hidden\") are excluded because they have no\n * visual or accessible representation.\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasAttribute,\n isFormInput,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that form inputs have an accessible label.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/input-requires-label.md',\n },\n messages: {\n missingLabel:\n 'Form input ({{ element }}) must have an accessible label. ' +\n 'Screen readers announce inputs by their label. Without one, ' +\n 'users hear \"edit text\" with no context. Add aria-label, ' +\n 'aria-labelledby, or associate a <label> element using htmlFor. ' +\n 'Note: placeholder is not a substitute for a label.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (!isFormInput(tagName)) return;\n\n // Hidden inputs have no visual or accessible representation\n const inputType = getAttributeValue(node, 'type');\n if (inputType === 'hidden') return;\n\n const hasAccessibleLabel =\n hasAttribute(node, 'aria-label') ||\n hasAttribute(node, 'aria-labelledby') ||\n hasAttribute(node, 'id');\n\n if (!hasAccessibleLabel) {\n context.report({\n node: astNode,\n messageId: 'missingLabel',\n data: { element: `<${tagName}>` },\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: radio-group-requires-grouping\n *\n * <input type=\"radio\"> must be inside a <fieldset> or an element\n * with role=\"radiogroup\".\n *\n * Ungrouped radio buttons are one of the most common form\n * accessibility failures. Without a grouping container, screen\n * readers announce each option independently: \"radio button, Red\"\n * followed by \"radio button, Blue\" with no indication that they\n * belong to the same question.\n *\n * A <fieldset> with <legend> provides: \"Color, group. Radio button,\n * Red. Radio button, Blue.\" The user immediately understands the\n * options are related and what question they answer.\n *\n * @see https://www.w3.org/WAI/tutorials/forms/grouping/\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport {\n getElementType,\n getAttributeValue,\n hasMatchingAncestor,\n} from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that radio buttons are inside a fieldset or role=\"radiogroup\".',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/radio-group-requires-grouping.md',\n },\n messages: {\n missingGrouping:\n 'Radio buttons must be grouped inside a <fieldset> with <legend> ' +\n 'or an element with role=\"radiogroup\" and aria-label. Without ' +\n 'grouping, screen readers announce each radio button independently ' +\n 'with no indication they belong to a set.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tagName = getElementType(node);\n\n if (tagName.toLowerCase() !== 'input') return;\n\n const inputType = getAttributeValue(node, 'type');\n if (inputType !== 'radio') return;\n\n const hasGroupingAncestor = hasMatchingAncestor(node, (ancestor) => {\n const ancestorTag = getElementType(ancestor);\n if (ancestorTag.toLowerCase() === 'fieldset') return true;\n if (getAttributeValue(ancestor, 'role') === 'radiogroup') return true;\n return false;\n });\n\n if (!hasGroupingAncestor) {\n context.report({ node: astNode, messageId: 'missingGrouping' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-positive-tabindex\n *\n * tabIndex must not be greater than 0.\n *\n * Positive tabIndex values override the natural DOM tab order.\n * An element with tabIndex={5} receives focus before all elements\n * with tabIndex={0}, regardless of its position in the document.\n * This creates an unpredictable navigation experience where the\n * focus jumps to seemingly random elements.\n *\n * jsx-a11y has this as a warning. We make it an error because there\n * is no legitimate use case for positive tabIndex in modern web\n * development. If you need an element to be focusable, use\n * tabIndex={0} (DOM order) or tabIndex={-1} (programmatic only).\n */\n\nimport type { Rule } from 'eslint';\nimport type { JSXOpeningElement } from '../types';\nimport { getNumericValue } from '../utils/ast-helpers';\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Enforce that tabIndex is not greater than 0.',\n url: 'https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce/blob/main/docs/rules/no-positive-tabindex.md',\n },\n messages: {\n positiveTabindex:\n 'tabIndex must not be greater than 0 (found tabIndex={{ value }}). ' +\n 'Positive tabIndex values break the natural tab order, creating ' +\n 'unpredictable keyboard navigation. Use tabIndex={0} to make an ' +\n 'element focusable in DOM order, or tabIndex={-1} for programmatic ' +\n 'focus only.',\n },\n schema: [],\n },\n\n create(context) {\n return {\n JSXOpeningElement(astNode: Rule.Node) {\n const node = astNode as unknown as JSXOpeningElement;\n const tabIndex = getNumericValue(node, 'tabIndex');\n\n if (tabIndex === undefined || tabIndex <= 0) return;\n\n context.report({\n node: astNode,\n messageId: 'positiveTabindex',\n data: { value: String(tabIndex) },\n });\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * eslint-plugin-a11y-enforce\n *\n * Catches accessibility composition errors that element-level tools\n * miss. Validates ARIA relationships in compound components (Dialog,\n * Menu, Select, Accordion, Tooltip) and common interaction patterns\n * (form labels, focus management, tab order).\n *\n * Designed to complement eslint-plugin-jsx-a11y, not replace it.\n *\n * @see https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce\n */\n\nimport type { Rule } from 'eslint';\n\nimport dialogRequiresModal from './rules/dialog-requires-modal';\nimport haspopupRoleMatch from './rules/haspopup-role-match';\nimport tooltipNoInteractive from './rules/tooltip-no-interactive';\nimport accordionTriggerHeading from './rules/accordion-trigger-heading';\nimport menuitemNotButton from './rules/menuitem-not-button';\nimport dialogRequiresTitle from './rules/dialog-requires-title';\nimport focusableHasInteraction from './rules/focusable-has-interaction';\nimport inputRequiresLabel from './rules/input-requires-label';\nimport radioGroupRequiresGrouping from './rules/radio-group-requires-grouping';\nimport noPositiveTabindex from './rules/no-positive-tabindex';\n\nconst rules: Record<string, Rule.RuleModule> = {\n 'dialog-requires-modal': dialogRequiresModal,\n 'haspopup-role-match': haspopupRoleMatch,\n 'tooltip-no-interactive': tooltipNoInteractive,\n 'accordion-trigger-heading': accordionTriggerHeading,\n 'menuitem-not-button': menuitemNotButton,\n 'dialog-requires-title': dialogRequiresTitle,\n 'focusable-has-interaction': focusableHasInteraction,\n 'input-requires-label': inputRequiresLabel,\n 'radio-group-requires-grouping': radioGroupRequiresGrouping,\n 'no-positive-tabindex': noPositiveTabindex,\n};\n\n/** All rules set to \"error\" for the recommended preset. */\nconst recommendedRules: Record<string, string> = Object.fromEntries(\n Object.keys(rules).map((name) => [`a11y-enforce/${name}`, 'error']),\n);\n\n// ESLint.Plugin's configs type is too narrow for dual ESLint 8/9 support.\n// Flat config uses { plugins: Record<string, Plugin> }, legacy uses\n// { plugins: string[] }. Both are valid but the union type doesn't satisfy\n// ESLint's typed config interface. We use a broader record type here\n// because the consumer picks one format based on their ESLint version.\nconst plugin = {\n meta: {\n name: 'eslint-plugin-a11y-enforce',\n version: '0.1.0',\n },\n rules,\n configs: {} as Record<string, Record<string, unknown>>,\n} satisfies { meta: { name: string; version: string }; rules: Record<string, Rule.RuleModule>; configs: Record<string, unknown> };\n\n// ESLint 9+ flat config: import and spread directly.\n// eslint.config.js: import a11yEnforce from 'eslint-plugin-a11y-enforce';\n// export default [a11yEnforce.configs.recommended];\nconst flatRecommended = {\n plugins: { 'a11y-enforce': plugin },\n rules: recommendedRules,\n};\n\n// ESLint 8 legacy config: extend the preset.\n// .eslintrc: { \"extends\": [\"plugin:a11y-enforce/recommended\"] }\nconst legacyRecommended = {\n plugins: ['a11y-enforce'],\n rules: recommendedRules,\n};\n\nplugin.configs = {\n recommended: flatRecommended,\n 'flat/recommended': flatRecommended,\n 'legacy/recommended': legacyRecommended,\n};\n\nexport default plugin;\nexport { rules, plugin };\n"],"mappings":";AAiBA,IAAM,uBAA4C,oBAAI,IAAI;AAAA,EACxD;AAAA,EAAK;AAAA,EAAU;AAAA,EAAS;AAAA,EAAU;AACpC,CAAC;AAED,IAAM,mBAAwC,oBAAI,IAAI;AAAA,EACpD;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAAA,EAAM;AAChC,CAAC;AAED,IAAM,sBAA2C,oBAAI,IAAI;AAAA,EACvD;AAAA,EAAS;AAAA,EAAU;AACrB,CAAC;AAMM,IAAM,oBAAyC,oBAAI,IAAI;AAAA,EAC5D;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAW;AAAA,EAC7B;AAAA,EAAS;AAAA,EAAY;AAAA,EAAY;AACnC,CAAC;AAQD,SAAS,cACP,MACA,UAC0B;AAC1B,aAAW,QAAQ,KAAK,YAAY;AAClC,QACE,KAAK,SAAS,kBACd,KAAK,KAAK,SAAS,mBACnB,KAAK,KAAK,SAAS,UACnB;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAUO,SAAS,kBACd,MACA,UACoB;AACpB,QAAM,OAAO,cAAc,MAAM,QAAQ;AACzC,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,KAAK,UAAU,KAAM,QAAO;AAEhC,MAAI,KAAK,MAAM,SAAS,aAAa,OAAO,KAAK,MAAM,UAAU,UAAU;AACzE,WAAO,KAAK,MAAM;AAAA,EACpB;AAEA,MACE,KAAK,MAAM,SAAS,4BACpB,KAAK,MAAM,WAAW,SAAS,aAC/B,OAAO,KAAK,MAAM,WAAW,UAAU,UACvC;AACA,WAAO,KAAK,MAAM,WAAW;AAAA,EAC/B;AAEA,SAAO;AACT;AASO,SAAS,gBACd,MACA,UACoB;AACpB,QAAM,OAAO,cAAc,MAAM,QAAQ;AACzC,MAAI,CAAC,QAAQ,KAAK,UAAU,KAAM,QAAO;AAEzC,MACE,KAAK,MAAM,SAAS,4BACpB,KAAK,MAAM,WAAW,SAAS,aAC/B,OAAO,KAAK,MAAM,WAAW,UAAU,UACvC;AACA,WAAO,KAAK,MAAM,WAAW;AAAA,EAC/B;AAEA,MAAI,KAAK,MAAM,SAAS,aAAa,OAAO,KAAK,MAAM,UAAU,UAAU;AACzE,UAAM,SAAS,SAAS,KAAK,MAAM,OAAO,EAAE;AAC5C,QAAI,CAAC,OAAO,MAAM,MAAM,EAAG,QAAO;AAAA,EACpC;AAEA,SAAO;AACT;AAGO,SAAS,aACd,MACA,UACS;AACT,SAAO,cAAc,MAAM,QAAQ,MAAM;AAC3C;AAGO,SAAS,mBACd,MACA,cACS;AACT,SAAO,aAAa,KAAK,CAAC,SAAS,aAAa,MAAM,IAAI,CAAC;AAC7D;AASO,SAAS,eAAe,MAAiC;AAC9D,MAAI,KAAK,KAAK,SAAS,mBAAmB,UAAU,KAAK,MAAM;AAC7D,WAAO,KAAK,KAAK,QAAQ;AAAA,EAC3B;AACA,SAAO;AACT;AAKO,SAAS,qBAAqB,SAA0B;AAC7D,SAAO,qBAAqB,IAAI,QAAQ,YAAY,CAAC;AACvD;AAOO,SAAS,iBACd,SACA,MACS;AACT,MAAI,iBAAiB,IAAI,QAAQ,YAAY,CAAC,EAAG,QAAO;AACxD,MAAI,QAAQ,kBAAkB,MAAM,MAAM,MAAM,UAAW,QAAO;AAClE,SAAO;AACT;AAGO,SAAS,YAAY,SAA0B;AACpD,SAAO,oBAAoB,IAAI,QAAQ,YAAY,CAAC;AACtD;AAWO,SAAS,oBACd,MACA,WACS;AAKT,MAAI,UAAU,KAAK;AAEnB,SAAO,SAAS;AACd,QAAI,QAAQ,SAAS,gBAAgB,QAAQ,gBAAgB;AAC3D,UAAI,UAAU,QAAQ,cAA8C,EAAG,QAAO;AAAA,IAChF;AACA,cAAW,QAAQ,UAAwD;AAAA,EAC7E;AAEA,SAAO;AACT;;;AC9LA,IAAM,eAAsC,CAAC,UAAU,aAAa;AAEpE,IAAM,OAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,OAAO,kBAAkB,MAAM,MAAM;AAE3C,YAAI,CAAC,QAAQ,CAAC,aAAa,SAAS,IAAI,EAAG;AAE3C,cAAM,YAAY,kBAAkB,MAAM,YAAY;AACtD,YAAI,cAAc,QAAQ;AACxB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,oBAAoB,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,QACjF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gCAAQ;;;ACjCf,IAAM,wBAA6C,oBAAI,IAAI;AAAA,EACzD;AAAA,EAAQ;AAAA,EAAW;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAU;AAAA,EAAQ;AACvD,CAAC;AAED,IAAMA,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,iBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,WAAW,kBAAkB,MAAM,eAAe;AAExD,YAAI,aAAa,OAAW;AAE5B,YAAI,CAAC,sBAAsB,IAAI,QAAQ,GAAG;AACxC,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,mBAAmB,MAAM,EAAE,OAAO,SAAS,EAAE,CAAC;AAAA,QAC3F;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,8BAAQA;;;AC3Bf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,sBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AAMd,aAAS,gBAAgB,MAAkC;AACzD,aAAO;AAAA,QACL;AAAA,QACA,CAAC,aAAa,kBAAkB,UAAU,MAAM,MAAM;AAAA,MACxD;AAAA,IACF;AAEA,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AAGb,YAAI,kBAAkB,MAAM,MAAM,MAAM,UAAW;AACnD,YAAI,CAAC,gBAAgB,IAAI,EAAG;AAE5B,cAAM,UAAU,eAAe,IAAI;AAGnC,YAAI,qBAAqB,OAAO,GAAG;AACjC,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,uBAAuB,CAAC;AACnE;AAAA,QACF;AAGA,cAAM,WAAW,gBAAgB,MAAM,UAAU;AACjD,YAAI,aAAa,UAAa,YAAY,GAAG;AAC3C,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,uBAAuB,CAAC;AACnE;AAAA,QACF;AAGA,cAAM,YAAY,kBAAkB,MAAM,MAAM;AAChD,YAAI,aAAa,kBAAkB,IAAI,SAAS,GAAG;AACjD,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,uBAAuB,CAAC;AAAA,QACrE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,iCAAQA;;;ACpEf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,gBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,UAAU,eAAe,IAAI;AAKnC,YAAI,QAAQ,YAAY,MAAM,SAAU;AACxC,YAAI,CAAC,aAAa,MAAM,eAAe,EAAG;AAE1C,cAAM,qBAAqB,oBAAoB,MAAM,CAAC,aAAa;AACjE,gBAAM,cAAc,eAAe,QAAQ;AAC3C,iBAAO,iBAAiB,aAAa,QAAQ;AAAA,QAC/C,CAAC;AAED,YAAI,CAAC,oBAAoB;AACvB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,iBAAiB,CAAC;AAAA,QAC/D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,oCAAQA;;;AC5Cf,IAAM,iBAAwC;AAAA,EAC5C;AAAA,EAAY;AAAA,EAAoB;AAClC;AAEA,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,OAAO,kBAAkB,MAAM,MAAM;AAE3C,YAAI,CAAC,QAAQ,CAAC,eAAe,SAAS,IAAI,EAAG;AAE7C,cAAM,UAAU,eAAe,IAAI;AACnC,YAAI,QAAQ,YAAY,MAAM,SAAU;AAExC,gBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,oBAAoB,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,MACjF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,8BAAQA;;;ACzCf,IAAMC,gBAAsC,CAAC,UAAU,aAAa;AAEpE,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,oBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,OAAO,kBAAkB,MAAM,MAAM;AAE3C,YAAI,CAAC,QAAQ,CAACD,cAAa,SAAS,IAAI,EAAG;AAE3C,cAAM,oBACJ,aAAa,MAAM,iBAAiB,KACpC,aAAa,MAAM,YAAY;AAEjC,YAAI,CAAC,mBAAmB;AACtB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,sBAAsB,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,QACnF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,gCAAQC;;;AC5Bf,IAAM,oBAA2C;AAAA,EAC/C;AAAA,EAAa;AAAA,EAAW;AAC1B;AAEA,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,wBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,WAAW,gBAAgB,MAAM,UAAU;AAGjD,YAAI,aAAa,EAAG;AAKpB,cAAM,UAAU,eAAe,IAAI;AACnC,YAAI,qBAAqB,OAAO,EAAG;AAEnC,YAAI,CAAC,mBAAmB,MAAM,iBAAiB,GAAG;AAChD,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,yBAAyB,CAAC;AAAA,QACvE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,oCAAQA;;;AC1Cf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,cACE;AAAA,IAKJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,UAAU,eAAe,IAAI;AAEnC,YAAI,CAAC,YAAY,OAAO,EAAG;AAG3B,cAAM,YAAY,kBAAkB,MAAM,MAAM;AAChD,YAAI,cAAc,SAAU;AAE5B,cAAM,qBACJ,aAAa,MAAM,YAAY,KAC/B,aAAa,MAAM,iBAAiB,KACpC,aAAa,MAAM,IAAI;AAEzB,YAAI,CAAC,oBAAoB;AACvB,kBAAQ,OAAO;AAAA,YACb,MAAM;AAAA,YACN,WAAW;AAAA,YACX,MAAM,EAAE,SAAS,IAAI,OAAO,IAAI;AAAA,UAClC,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,+BAAQA;;;ACnDf,IAAMC,QAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,iBACE;AAAA,IAIJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,UAAU,eAAe,IAAI;AAEnC,YAAI,QAAQ,YAAY,MAAM,QAAS;AAEvC,cAAM,YAAY,kBAAkB,MAAM,MAAM;AAChD,YAAI,cAAc,QAAS;AAE3B,cAAM,sBAAsB,oBAAoB,MAAM,CAAC,aAAa;AAClE,gBAAM,cAAc,eAAe,QAAQ;AAC3C,cAAI,YAAY,YAAY,MAAM,WAAY,QAAO;AACrD,cAAI,kBAAkB,UAAU,MAAM,MAAM,aAAc,QAAO;AACjE,iBAAO;AAAA,QACT,CAAC;AAED,YAAI,CAAC,qBAAqB;AACxB,kBAAQ,OAAO,EAAE,MAAM,SAAS,WAAW,kBAAkB,CAAC;AAAA,QAChE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,wCAAQA;;;ACjDf,IAAMC,SAAwB;AAAA,EAC5B,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,KAAK;AAAA,IACP;AAAA,IACA,UAAU;AAAA,MACR,kBACE;AAAA,IAKJ;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EAEA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,SAAoB;AACpC,cAAM,OAAO;AACb,cAAM,WAAW,gBAAgB,MAAM,UAAU;AAEjD,YAAI,aAAa,UAAa,YAAY,EAAG;AAE7C,gBAAQ,OAAO;AAAA,UACb,MAAM;AAAA,UACN,WAAW;AAAA,UACX,MAAM,EAAE,OAAO,OAAO,QAAQ,EAAE;AAAA,QAClC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACF;AAEA,IAAO,+BAAQA;;;AC/Bf,IAAM,QAAyC;AAAA,EAC7C,yBAAyB;AAAA,EACzB,uBAAuB;AAAA,EACvB,0BAA0B;AAAA,EAC1B,6BAA6B;AAAA,EAC7B,uBAAuB;AAAA,EACvB,yBAAyB;AAAA,EACzB,6BAA6B;AAAA,EAC7B,wBAAwB;AAAA,EACxB,iCAAiC;AAAA,EACjC,wBAAwB;AAC1B;AAGA,IAAM,mBAA2C,OAAO;AAAA,EACtD,OAAO,KAAK,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,gBAAgB,IAAI,IAAI,OAAO,CAAC;AACpE;AAOA,IAAM,SAAS;AAAA,EACb,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AAAA,EACA;AAAA,EACA,SAAS,CAAC;AACZ;AAKA,IAAM,kBAAkB;AAAA,EACtB,SAAS,EAAE,gBAAgB,OAAO;AAAA,EAClC,OAAO;AACT;AAIA,IAAM,oBAAoB;AAAA,EACxB,SAAS,CAAC,cAAc;AAAA,EACxB,OAAO;AACT;AAEA,OAAO,UAAU;AAAA,EACf,aAAa;AAAA,EACb,oBAAoB;AAAA,EACpB,sBAAsB;AACxB;AAEA,IAAO,gBAAQ;","names":["rule","rule","rule","rule","DIALOG_ROLES","rule","rule","rule","rule","rule"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "eslint-plugin-a11y-enforce",
3
+ "version": "0.1.0",
4
+ "description": "ESLint plugin that catches accessibility composition errors that element-level tools miss.",
5
+ "author": "Venkatesh Mukundan <vmvenkatesh78@gmail.com>",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/vmvenkatesh78/eslint-plugin-a11y-enforce"
10
+ },
11
+ "keywords": [
12
+ "eslint",
13
+ "eslintplugin",
14
+ "eslint-plugin",
15
+ "accessibility",
16
+ "a11y",
17
+ "aria",
18
+ "wcag",
19
+ "react",
20
+ "jsx"
21
+ ],
22
+ "type": "module",
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js",
30
+ "require": "./dist/index.cjs"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "dev": "tsup --watch",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "typecheck": "tsc --noEmit",
44
+ "lint": "eslint src/",
45
+ "clean": "rm -rf dist",
46
+ "prepublishOnly": "pnpm clean && pnpm build"
47
+ },
48
+ "peerDependencies": {
49
+ "eslint": ">=8.0.0"
50
+ },
51
+ "devDependencies": {
52
+ "@eslint/js": "^9.39.4",
53
+ "@types/node": "^22.0.0",
54
+ "eslint": "^9.0.0",
55
+ "tsup": "^8.0.0",
56
+ "typescript": "^5.5.0",
57
+ "typescript-eslint": "^8.58.0",
58
+ "vitest": "^2.0.0"
59
+ },
60
+ "engines": {
61
+ "node": ">=18.0.0"
62
+ },
63
+ "packageManager": "pnpm@9.15.0"
64
+ }