@tanstack/devtools-a11y 0.0.1 → 0.1.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.
Files changed (110) hide show
  1. package/LICENSE +21 -0
  2. package/dist/esm/core/components/IssueCard.d.ts +10 -0
  3. package/dist/esm/core/components/IssueCard.js +83 -0
  4. package/dist/esm/core/components/IssueCard.js.map +1 -0
  5. package/dist/esm/core/components/IssueList.d.ts +6 -0
  6. package/dist/esm/core/components/IssueList.js +134 -0
  7. package/dist/esm/core/components/IssueList.js.map +1 -0
  8. package/dist/esm/core/components/Settings.d.ts +6 -0
  9. package/dist/esm/core/components/Settings.js +251 -0
  10. package/dist/esm/core/components/Settings.js.map +1 -0
  11. package/dist/esm/core/components/Shell.d.ts +2 -0
  12. package/dist/esm/core/components/Shell.js +214 -0
  13. package/dist/esm/core/components/Shell.js.map +1 -0
  14. package/dist/esm/core/components/index.d.ts +2 -0
  15. package/dist/esm/core/components/index.js +14 -0
  16. package/dist/esm/core/components/index.js.map +1 -0
  17. package/dist/esm/core/contexts/allyContext.d.ts +17 -0
  18. package/dist/esm/core/contexts/allyContext.js +66 -0
  19. package/dist/esm/core/contexts/allyContext.js.map +1 -0
  20. package/dist/esm/core/core.d.ts +19 -0
  21. package/dist/esm/core/core.js +8 -0
  22. package/dist/esm/core/core.js.map +1 -0
  23. package/dist/esm/core/index.d.ts +9 -0
  24. package/dist/esm/core/index.js +9 -0
  25. package/dist/esm/core/index.js.map +1 -0
  26. package/dist/esm/core/production.d.ts +2 -0
  27. package/dist/esm/core/production.js +4 -0
  28. package/dist/esm/core/styles/styles.d.ts +85 -0
  29. package/dist/esm/core/styles/styles.js +547 -0
  30. package/dist/esm/core/styles/styles.js.map +1 -0
  31. package/dist/esm/core/types/types.d.ts +141 -0
  32. package/dist/esm/core/utils/ally-audit.utils.d.ts +19 -0
  33. package/dist/esm/core/utils/ally-audit.utils.js +226 -0
  34. package/dist/esm/core/utils/ally-audit.utils.js.map +1 -0
  35. package/dist/esm/core/utils/config.utils.d.ts +17 -0
  36. package/dist/esm/core/utils/config.utils.js +63 -0
  37. package/dist/esm/core/utils/config.utils.js.map +1 -0
  38. package/dist/esm/core/utils/custom-audit.utils.d.ts +13 -0
  39. package/dist/esm/core/utils/custom-audit.utils.js +426 -0
  40. package/dist/esm/core/utils/custom-audit.utils.js.map +1 -0
  41. package/dist/esm/core/utils/export-audit.uitls.d.ts +17 -0
  42. package/dist/esm/core/utils/export-audit.uitls.js +83 -0
  43. package/dist/esm/core/utils/export-audit.uitls.js.map +1 -0
  44. package/dist/esm/core/utils/ui.utils.d.ts +24 -0
  45. package/dist/esm/core/utils/ui.utils.js +330 -0
  46. package/dist/esm/core/utils/ui.utils.js.map +1 -0
  47. package/dist/esm/react/A11yDevtools.d.ts +5 -0
  48. package/dist/esm/react/A11yDevtools.js +8 -0
  49. package/dist/esm/react/A11yDevtools.js.map +1 -0
  50. package/dist/esm/react/index.d.ts +8 -0
  51. package/dist/esm/react/index.js +11 -0
  52. package/dist/esm/react/index.js.map +1 -0
  53. package/dist/esm/react/plugin.d.ts +12 -0
  54. package/dist/esm/react/plugin.js +11 -0
  55. package/dist/esm/react/plugin.js.map +1 -0
  56. package/dist/esm/react/production/A11yDevtools.d.ts +5 -0
  57. package/dist/esm/react/production/A11yDevtools.js +8 -0
  58. package/dist/esm/react/production/A11yDevtools.js.map +1 -0
  59. package/dist/esm/react/production/plugin.d.ts +7 -0
  60. package/dist/esm/react/production/plugin.js +11 -0
  61. package/dist/esm/react/production/plugin.js.map +1 -0
  62. package/dist/esm/react/production.d.ts +3 -0
  63. package/dist/esm/react/production.js +5 -0
  64. package/dist/esm/solid/A11yDevtools.d.ts +5 -0
  65. package/dist/esm/solid/A11yDevtools.js +8 -0
  66. package/dist/esm/solid/A11yDevtools.js.map +1 -0
  67. package/dist/esm/solid/index.d.ts +8 -0
  68. package/dist/esm/solid/index.js +9 -0
  69. package/dist/esm/solid/index.js.map +1 -0
  70. package/dist/esm/solid/plugin.d.ts +12 -0
  71. package/dist/esm/solid/plugin.js +11 -0
  72. package/dist/esm/solid/plugin.js.map +1 -0
  73. package/dist/esm/solid/production/A11yDevtools.d.ts +5 -0
  74. package/dist/esm/solid/production/A11yDevtools.js +8 -0
  75. package/dist/esm/solid/production/A11yDevtools.js.map +1 -0
  76. package/dist/esm/solid/production/plugin.d.ts +7 -0
  77. package/dist/esm/solid/production/plugin.js +11 -0
  78. package/dist/esm/solid/production/plugin.js.map +1 -0
  79. package/dist/esm/solid/production.d.ts +3 -0
  80. package/dist/esm/solid/production.js +3 -0
  81. package/package.json +110 -7
  82. package/src/core/components/IssueCard.tsx +75 -0
  83. package/src/core/components/IssueList.tsx +155 -0
  84. package/src/core/components/Settings.tsx +221 -0
  85. package/src/core/components/Shell.tsx +154 -0
  86. package/src/core/components/index.tsx +12 -0
  87. package/src/core/contexts/allyContext.tsx +118 -0
  88. package/src/core/core.tsx +11 -0
  89. package/src/core/index.ts +10 -0
  90. package/src/core/production.ts +5 -0
  91. package/src/core/styles/styles.ts +556 -0
  92. package/src/core/types/types.ts +177 -0
  93. package/src/core/utils/ally-audit.utils.ts +345 -0
  94. package/src/core/utils/config.utils.ts +68 -0
  95. package/src/core/utils/custom-audit.utils.ts +643 -0
  96. package/src/core/utils/export-audit.uitls.ts +180 -0
  97. package/src/core/utils/ui.utils.ts +483 -0
  98. package/src/react/A11yDevtools.ts +12 -0
  99. package/src/react/index.ts +16 -0
  100. package/src/react/plugin.ts +9 -0
  101. package/src/react/production/A11yDevtools.ts +11 -0
  102. package/src/react/production/plugin.ts +9 -0
  103. package/src/react/production.ts +7 -0
  104. package/src/solid/A11yDevtools.ts +11 -0
  105. package/src/solid/index.ts +14 -0
  106. package/src/solid/plugin.ts +9 -0
  107. package/src/solid/production/A11yDevtools.ts +10 -0
  108. package/src/solid/production/plugin.ts +9 -0
  109. package/src/solid/production.ts +5 -0
  110. package/README.md +0 -45
@@ -0,0 +1,426 @@
1
+ import { meetsThreshold } from "./ally-audit.utils.js";
2
+ //#region src/core/utils/custom-audit.utils.ts
3
+ /**
4
+ * Custom accessibility rules for issues not covered by axe-core
5
+ *
6
+ * These rules detect common accessibility anti-patterns like:
7
+ * - Click handlers on non-interactive elements
8
+ * - Mouse-only event handlers without keyboard equivalents
9
+ * - Static elements with interactive handlers
10
+ */
11
+ /**
12
+ * Interactive HTML elements that can receive focus and have implicit roles
13
+ */
14
+ var INTERACTIVE_ELEMENTS = new Set([
15
+ "a",
16
+ "button",
17
+ "input",
18
+ "select",
19
+ "textarea",
20
+ "details",
21
+ "summary",
22
+ "audio",
23
+ "video"
24
+ ]);
25
+ /**
26
+ * Elements that are interactive when they have an href attribute
27
+ */
28
+ var INTERACTIVE_WITH_HREF = new Set(["a", "area"]);
29
+ /**
30
+ * Interactive ARIA roles
31
+ */
32
+ var INTERACTIVE_ROLES = new Set([
33
+ "button",
34
+ "checkbox",
35
+ "combobox",
36
+ "gridcell",
37
+ "link",
38
+ "listbox",
39
+ "menu",
40
+ "menubar",
41
+ "menuitem",
42
+ "menuitemcheckbox",
43
+ "menuitemradio",
44
+ "option",
45
+ "progressbar",
46
+ "radio",
47
+ "scrollbar",
48
+ "searchbox",
49
+ "slider",
50
+ "spinbutton",
51
+ "switch",
52
+ "tab",
53
+ "tabpanel",
54
+ "textbox",
55
+ "tree",
56
+ "treeitem"
57
+ ]);
58
+ /**
59
+ * Mouse-only events that should have keyboard equivalents
60
+ */
61
+ var MOUSE_ONLY_EVENTS = [
62
+ "onclick",
63
+ "ondblclick",
64
+ "onmousedown",
65
+ "onmouseup",
66
+ "onmouseover",
67
+ "onmouseout",
68
+ "onmouseenter",
69
+ "onmouseleave"
70
+ ];
71
+ /**
72
+ * Keyboard events that would make an element accessible
73
+ */
74
+ var KEYBOARD_EVENTS = [
75
+ "onkeydown",
76
+ "onkeyup",
77
+ "onkeypress"
78
+ ];
79
+ /**
80
+ * Selectors for devtools elements to exclude
81
+ */
82
+ var DEVTOOLS_SELECTORS = [
83
+ "[data-testid=\"tanstack_devtools\"]",
84
+ "[data-devtools]",
85
+ "[data-devtools-panel]",
86
+ "[data-a11y-overlay]"
87
+ ];
88
+ /**
89
+ * Common root container element IDs used by frameworks.
90
+ * React attaches event delegation to these elements, which would
91
+ * cause false positives for click handler detection.
92
+ */
93
+ var ROOT_CONTAINER_IDS = new Set([
94
+ "root",
95
+ "app",
96
+ "__next",
97
+ "__nuxt",
98
+ "__gatsby",
99
+ "app-root",
100
+ "svelte",
101
+ "q-app"
102
+ ]);
103
+ /**
104
+ * Check if an element is a root container (framework app mount point).
105
+ * These elements often have React internals attached for event delegation
106
+ * but don't actually have user-defined click handlers.
107
+ */
108
+ function isRootContainer(element) {
109
+ if (element.id && ROOT_CONTAINER_IDS.has(element.id)) return true;
110
+ if (element.parentElement === document.body) {
111
+ const tagName = element.tagName.toLowerCase();
112
+ if (tagName === "div" || tagName === "main" || tagName === "section") {
113
+ const keys = Object.keys(element);
114
+ for (const key of keys) if (key.startsWith("__reactProps$")) {
115
+ const props = element[key];
116
+ if (props && typeof props === "object") {
117
+ const propsObj = props;
118
+ if ("children" in propsObj && !("onClick" in propsObj)) return true;
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return false;
124
+ }
125
+ /**
126
+ * Check if an element is inside devtools
127
+ */
128
+ function isInsideDevtools(element) {
129
+ for (const selector of DEVTOOLS_SELECTORS) if (element.closest(selector)) return true;
130
+ return false;
131
+ }
132
+ /**
133
+ * Check if element is interactive by nature
134
+ */
135
+ function isInteractiveElement(element) {
136
+ const tagName = element.tagName.toLowerCase();
137
+ if (INTERACTIVE_ELEMENTS.has(tagName)) return !element.hasAttribute("disabled");
138
+ return INTERACTIVE_WITH_HREF.has(tagName) && element.hasAttribute("href");
139
+ }
140
+ /**
141
+ * Check if element has an interactive ARIA role
142
+ */
143
+ function hasInteractiveRole(element) {
144
+ const role = element.getAttribute("role");
145
+ return role !== null && INTERACTIVE_ROLES.has(role);
146
+ }
147
+ /**
148
+ * Check if element is focusable (has tabindex)
149
+ */
150
+ function isFocusable(element) {
151
+ const tabindex = element.getAttribute("tabindex");
152
+ if (tabindex === null) return false;
153
+ const tabindexValue = parseInt(tabindex, 10);
154
+ return !isNaN(tabindexValue) && tabindexValue >= 0;
155
+ }
156
+ /**
157
+ * Check if element has click event handlers (via attribute or property)
158
+ */
159
+ function hasClickHandler(element) {
160
+ if (element.hasAttribute("onclick")) return true;
161
+ if (typeof element.onclick === "function") return true;
162
+ const keys = Object.keys(element);
163
+ for (const key of keys) if (key.startsWith("__reactProps$") || key.startsWith("__reactFiber$") || key.startsWith("__reactEventHandlers$")) {
164
+ const props = element[key];
165
+ if (props && typeof props === "object") {
166
+ const propsObj = props;
167
+ if (typeof propsObj.onClick === "function" || typeof propsObj.onMouseDown === "function" || typeof propsObj.onMouseUp === "function") return true;
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+ /**
173
+ * Check if element has keyboard event handlers
174
+ */
175
+ function hasKeyboardHandler(element) {
176
+ for (const event of KEYBOARD_EVENTS) if (element.hasAttribute(event)) return true;
177
+ const htmlElement = element;
178
+ if (typeof htmlElement.onkeydown === "function" || typeof htmlElement.onkeyup === "function" || typeof htmlElement.onkeypress === "function") return true;
179
+ const keys = Object.keys(element);
180
+ for (const key of keys) if (key.startsWith("__reactProps$")) {
181
+ const props = element[key];
182
+ if (props && typeof props === "object") {
183
+ const propsObj = props;
184
+ if (typeof propsObj.onKeyDown === "function" || typeof propsObj.onKeyUp === "function" || typeof propsObj.onKeyPress === "function") return true;
185
+ }
186
+ }
187
+ return false;
188
+ }
189
+ /**
190
+ * Class prefixes to exclude from selectors (devtools overlay classes)
191
+ */
192
+ var EXCLUDED_CLASS_PREFIXES = ["tsd-a11y-"];
193
+ /**
194
+ * Filter out devtools-injected classes from class list
195
+ */
196
+ function filterClasses(classList) {
197
+ return Array.from(classList).filter((cls) => !EXCLUDED_CLASS_PREFIXES.some((prefix) => cls.startsWith(prefix)));
198
+ }
199
+ /**
200
+ * Get a unique selector for an element
201
+ */
202
+ function getSelector(element) {
203
+ if (element.id) return `#${element.id}`;
204
+ const tagName = element.tagName.toLowerCase();
205
+ const classes = filterClasses(element.classList).join(".");
206
+ const classSelector = classes ? `.${classes}` : "";
207
+ const parent = element.parentElement;
208
+ if (parent && parent !== document.body) {
209
+ const parentSelector = getSelector(parent);
210
+ const siblings = Array.from(parent.children).filter((el) => el.tagName === element.tagName);
211
+ if (siblings.length > 1) return `${parentSelector} > ${tagName}${classSelector}:nth-of-type(${siblings.indexOf(element) + 1})`;
212
+ return `${parentSelector} > ${tagName}${classSelector}`;
213
+ }
214
+ return `${tagName}${classSelector}`;
215
+ }
216
+ /**
217
+ * Custom rule: Click handler on non-interactive element
218
+ *
219
+ * This rule detects elements that have click handlers but are not:
220
+ * - Interactive HTML elements (button, a, input, etc.)
221
+ * - Elements with interactive ARIA roles
222
+ * - Elements with tabindex for keyboard access
223
+ */
224
+ function checkClickHandlerOnNonInteractive(context = document, threshold = "serious") {
225
+ const issues = [];
226
+ const timestamp = Date.now();
227
+ const allElements = context.querySelectorAll("*");
228
+ for (const element of allElements) {
229
+ if (isInsideDevtools(element)) continue;
230
+ if (isRootContainer(element)) continue;
231
+ if (isInteractiveElement(element) || hasInteractiveRole(element)) continue;
232
+ if (!hasClickHandler(element)) continue;
233
+ const hasFocus = isFocusable(element);
234
+ const hasKeyboard = hasKeyboardHandler(element);
235
+ if (!hasFocus && !hasKeyboard) {
236
+ const selector = getSelector(element);
237
+ issues.push({
238
+ id: `click-handler-no-keyboard-${timestamp}-${issues.length}`,
239
+ ruleId: "click-handler-on-non-interactive",
240
+ impact: "serious",
241
+ message: "Element has a click handler but is not keyboard accessible. Add tabindex=\"0\" and keyboard event handlers, or use an interactive element like <button>.",
242
+ help: "Interactive elements must be keyboard accessible",
243
+ helpUrl: "https://www.w3.org/WAI/WCAG21/Understanding/keyboard-accessible",
244
+ wcagTags: ["wcag211", "wcag21a"],
245
+ nodes: [{
246
+ selector,
247
+ html: element.outerHTML.slice(0, 200)
248
+ }],
249
+ meetsThreshold: meetsThreshold("serious", threshold),
250
+ timestamp
251
+ });
252
+ } else if (hasFocus && !hasKeyboard) {
253
+ const selector = getSelector(element);
254
+ issues.push({
255
+ id: `click-handler-no-keyboard-handler-${timestamp}-${issues.length}`,
256
+ ruleId: "click-handler-on-non-interactive",
257
+ impact: "moderate",
258
+ message: "Element has a click handler and tabindex but no keyboard event handler. Add onKeyDown/onKeyPress to handle Enter/Space keys.",
259
+ help: "Interactive elements should respond to keyboard events",
260
+ helpUrl: "https://www.w3.org/WAI/WCAG21/Understanding/keyboard-accessible",
261
+ wcagTags: ["wcag211", "wcag21a"],
262
+ nodes: [{
263
+ selector,
264
+ html: element.outerHTML.slice(0, 200)
265
+ }],
266
+ meetsThreshold: meetsThreshold("moderate", threshold),
267
+ timestamp
268
+ });
269
+ }
270
+ }
271
+ return issues;
272
+ }
273
+ /**
274
+ * Custom rule: Mouse-only event handlers
275
+ *
276
+ * Detects elements that have mouse event handlers (onmouseover, onmousedown, etc.)
277
+ * without corresponding keyboard event handlers.
278
+ */
279
+ function checkMouseOnlyEvents(context = document, threshold = "serious") {
280
+ const issues = [];
281
+ const timestamp = Date.now();
282
+ const mouseEventSelectors = MOUSE_ONLY_EVENTS.map((event) => `[${event}]`).join(", ");
283
+ const elements = context.querySelectorAll(mouseEventSelectors);
284
+ for (const element of elements) {
285
+ if (isInsideDevtools(element)) continue;
286
+ if (isInteractiveElement(element)) continue;
287
+ if (hasKeyboardHandler(element) || isFocusable(element)) continue;
288
+ const mouseEvents = [];
289
+ for (const event of MOUSE_ONLY_EVENTS) if (element.hasAttribute(event)) mouseEvents.push(event);
290
+ const selector = getSelector(element);
291
+ issues.push({
292
+ id: `mouse-only-events-${timestamp}-${issues.length}`,
293
+ ruleId: "mouse-only-event-handlers",
294
+ impact: "serious",
295
+ message: `Element has mouse-only event handlers (${mouseEvents.join(", ")}) without keyboard equivalents. Ensure functionality is available via keyboard.`,
296
+ help: "All functionality must be operable through keyboard",
297
+ helpUrl: "https://www.w3.org/WAI/WCAG21/Understanding/keyboard",
298
+ wcagTags: ["wcag211", "wcag21a"],
299
+ nodes: [{
300
+ selector,
301
+ html: element.outerHTML.slice(0, 200)
302
+ }],
303
+ meetsThreshold: meetsThreshold("serious", threshold),
304
+ timestamp
305
+ });
306
+ }
307
+ return issues;
308
+ }
309
+ /**
310
+ * Custom rule: Static element with interactive semantics
311
+ *
312
+ * Detects elements like <div> or <span> that have role="button" but lack
313
+ * proper keyboard handling (tabindex and key events).
314
+ */
315
+ function checkStaticElementInteraction(context = document, threshold = "serious") {
316
+ const issues = [];
317
+ const timestamp = Date.now();
318
+ const roleSelectors = Array.from(INTERACTIVE_ROLES).map((role) => `[role="${role}"]`).join(", ");
319
+ const elements = context.querySelectorAll(roleSelectors);
320
+ for (const element of elements) {
321
+ if (isInsideDevtools(element)) continue;
322
+ if (isInteractiveElement(element)) continue;
323
+ const role = element.getAttribute("role");
324
+ const hasFocus = isFocusable(element);
325
+ const hasKeyboard = hasKeyboardHandler(element);
326
+ if (!hasFocus) {
327
+ const selector = getSelector(element);
328
+ issues.push({
329
+ id: `static-element-no-tabindex-${timestamp}-${issues.length}`,
330
+ ruleId: "static-element-interaction",
331
+ impact: "serious",
332
+ message: `Element with role="${role}" is not focusable. Add tabindex="0" to make it keyboard accessible.`,
333
+ help: "Elements with interactive roles must be focusable",
334
+ helpUrl: "https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA4#description",
335
+ wcagTags: [
336
+ "wcag211",
337
+ "wcag21a",
338
+ "wcag412"
339
+ ],
340
+ nodes: [{
341
+ selector,
342
+ html: element.outerHTML.slice(0, 200)
343
+ }],
344
+ meetsThreshold: meetsThreshold("serious", threshold),
345
+ timestamp
346
+ });
347
+ }
348
+ if (role && [
349
+ "button",
350
+ "link",
351
+ "menuitem",
352
+ "option"
353
+ ].includes(role) && !hasKeyboard && hasClickHandler(element)) {
354
+ const selector = getSelector(element);
355
+ issues.push({
356
+ id: `static-element-no-keyboard-${timestamp}-${issues.length}`,
357
+ ruleId: "static-element-interaction",
358
+ impact: "moderate",
359
+ message: `Element with role="${role}" has click handler but no keyboard handler. Add onKeyDown to handle Enter/Space.`,
360
+ help: "Elements with button-like roles should respond to Enter and Space keys",
361
+ helpUrl: "https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA4#description",
362
+ wcagTags: ["wcag211", "wcag21a"],
363
+ nodes: [{
364
+ selector,
365
+ html: element.outerHTML.slice(0, 200)
366
+ }],
367
+ meetsThreshold: meetsThreshold("moderate", threshold),
368
+ timestamp
369
+ });
370
+ }
371
+ }
372
+ return issues;
373
+ }
374
+ /**
375
+ * Run all enabled custom rules
376
+ */
377
+ function runCustomRules(context = document, config = {}, threshold = "serious") {
378
+ const { clickHandlerOnNonInteractive = true, mouseOnlyEventHandlers = true, staticElementInteraction = true } = config;
379
+ const issues = [];
380
+ if (clickHandlerOnNonInteractive) issues.push(...checkClickHandlerOnNonInteractive(context, threshold));
381
+ if (mouseOnlyEventHandlers) issues.push(...checkMouseOnlyEvents(context, threshold));
382
+ if (staticElementInteraction) issues.push(...checkStaticElementInteraction(context, threshold));
383
+ return issues;
384
+ }
385
+ /**
386
+ * Get list of custom rule metadata (for UI display)
387
+ */
388
+ function getCustomRules() {
389
+ return [
390
+ {
391
+ id: "click-handler-on-non-interactive",
392
+ description: "Ensures click handlers are only on keyboard-accessible elements",
393
+ tags: [
394
+ "custom",
395
+ "cat.keyboard",
396
+ "wcag21a",
397
+ "wcag211"
398
+ ]
399
+ },
400
+ {
401
+ id: "mouse-only-event-handlers",
402
+ description: "Ensures mouse event handlers have keyboard equivalents",
403
+ tags: [
404
+ "custom",
405
+ "cat.keyboard",
406
+ "wcag21a",
407
+ "wcag211"
408
+ ]
409
+ },
410
+ {
411
+ id: "static-element-interaction",
412
+ description: "Ensures elements with interactive roles are properly keyboard accessible",
413
+ tags: [
414
+ "custom",
415
+ "cat.keyboard",
416
+ "cat.aria",
417
+ "wcag21a",
418
+ "wcag211"
419
+ ]
420
+ }
421
+ ];
422
+ }
423
+ //#endregion
424
+ export { getCustomRules, runCustomRules };
425
+
426
+ //# sourceMappingURL=custom-audit.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-audit.utils.js","names":[],"sources":["../../../../src/core/utils/custom-audit.utils.ts"],"sourcesContent":["/**\n * Custom accessibility rules for issues not covered by axe-core\n *\n * These rules detect common accessibility anti-patterns like:\n * - Click handlers on non-interactive elements\n * - Mouse-only event handlers without keyboard equivalents\n * - Static elements with interactive handlers\n */\n\nimport { meetsThreshold } from './ally-audit.utils'\n\nimport type {\n A11yIssue,\n CustomRulesConfig,\n SeverityThreshold,\n} from '../types/types'\n\n/**\n * Interactive HTML elements that can receive focus and have implicit roles\n */\nconst INTERACTIVE_ELEMENTS = new Set([\n 'a',\n 'button',\n 'input',\n 'select',\n 'textarea',\n 'details',\n 'summary',\n 'audio',\n 'video',\n])\n\n/**\n * Elements that are interactive when they have an href attribute\n */\nconst INTERACTIVE_WITH_HREF = new Set(['a', 'area'])\n\n/**\n * Interactive ARIA roles\n */\nconst INTERACTIVE_ROLES = new Set([\n 'button',\n 'checkbox',\n 'combobox',\n 'gridcell',\n 'link',\n 'listbox',\n 'menu',\n 'menubar',\n 'menuitem',\n 'menuitemcheckbox',\n 'menuitemradio',\n 'option',\n 'progressbar',\n 'radio',\n 'scrollbar',\n 'searchbox',\n 'slider',\n 'spinbutton',\n 'switch',\n 'tab',\n 'tabpanel',\n 'textbox',\n 'tree',\n 'treeitem',\n])\n\n/**\n * Mouse-only events that should have keyboard equivalents\n */\nconst MOUSE_ONLY_EVENTS = [\n 'onclick',\n 'ondblclick',\n 'onmousedown',\n 'onmouseup',\n 'onmouseover',\n 'onmouseout',\n 'onmouseenter',\n 'onmouseleave',\n]\n\n/**\n * Keyboard events that would make an element accessible\n */\nconst KEYBOARD_EVENTS = ['onkeydown', 'onkeyup', 'onkeypress']\n\n/**\n * Selectors for devtools elements to exclude\n */\nconst DEVTOOLS_SELECTORS = [\n '[data-testid=\"tanstack_devtools\"]',\n '[data-devtools]',\n '[data-devtools-panel]',\n '[data-a11y-overlay]',\n]\n\n/**\n * Common root container element IDs used by frameworks.\n * React attaches event delegation to these elements, which would\n * cause false positives for click handler detection.\n */\nconst ROOT_CONTAINER_IDS = new Set([\n 'root',\n 'app',\n '__next', // Next.js\n '__nuxt', // Nuxt\n '__gatsby', // Gatsby\n 'app-root', // Angular\n 'svelte', // SvelteKit\n 'q-app', // Qwik\n])\n\n/**\n * Check if an element is a root container (framework app mount point).\n * These elements often have React internals attached for event delegation\n * but don't actually have user-defined click handlers.\n */\nfunction isRootContainer(element: Element): boolean {\n // Check by ID\n if (element.id && ROOT_CONTAINER_IDS.has(element.id)) {\n return true\n }\n\n // Check if direct child of body (common for app containers)\n if (element.parentElement === document.body) {\n // Only consider it a root if it has no meaningful content attributes\n // that would indicate it's an interactive element\n const tagName = element.tagName.toLowerCase()\n if (tagName === 'div' || tagName === 'main' || tagName === 'section') {\n // Check if this looks like an app container (wraps most of the page)\n // by checking if it has React fiber but no explicit onClick in props\n const keys = Object.keys(element)\n for (const key of keys) {\n if (key.startsWith('__reactProps$')) {\n const props = (element as unknown as Record<string, unknown>)[key]\n if (props && typeof props === 'object') {\n const propsObj = props as Record<string, unknown>\n // If it has children but no onClick, it's likely a container\n if ('children' in propsObj && !('onClick' in propsObj)) {\n return true\n }\n }\n }\n }\n }\n }\n\n return false\n}\n\n/**\n * Check if an element is inside devtools\n */\nfunction isInsideDevtools(element: Element): boolean {\n for (const selector of DEVTOOLS_SELECTORS) {\n if (element.closest(selector)) {\n return true\n }\n }\n return false\n}\n\n/**\n * Check if element is interactive by nature\n */\nfunction isInteractiveElement(element: Element): boolean {\n const tagName = element.tagName.toLowerCase()\n\n // Check if it's an inherently interactive element\n if (INTERACTIVE_ELEMENTS.has(tagName)) {\n // Disabled elements are not interactive\n return !element.hasAttribute('disabled')\n }\n\n // Check if it's an element that becomes interactive with href\n return INTERACTIVE_WITH_HREF.has(tagName) && element.hasAttribute('href')\n}\n\n/**\n * Check if element has an interactive ARIA role\n */\nfunction hasInteractiveRole(element: Element): boolean {\n const role = element.getAttribute('role')\n return role !== null && INTERACTIVE_ROLES.has(role)\n}\n\n/**\n * Check if element is focusable (has tabindex)\n */\nfunction isFocusable(element: Element): boolean {\n const tabindex = element.getAttribute('tabindex')\n if (tabindex === null) {\n return false\n }\n const tabindexValue = parseInt(tabindex, 10)\n return !isNaN(tabindexValue) && tabindexValue >= 0\n}\n\n/**\n * Check if element has click event handlers (via attribute or property)\n */\nfunction hasClickHandler(element: Element): boolean {\n // Check for onclick attribute\n if (element.hasAttribute('onclick')) {\n return true\n }\n\n // Check for event listener via property (common in React/frameworks)\n // Note: We can't detect addEventListener calls, but we can check common patterns\n const htmlElement = element as HTMLElement\n\n // Check if onclick property is set\n if (typeof htmlElement.onclick === 'function') {\n return true\n }\n\n // Check for React synthetic events (data attributes often indicate handlers)\n // React 17+ uses __reactFiber$ and __reactProps$ prefixed properties\n const keys = Object.keys(element)\n for (const key of keys) {\n if (\n key.startsWith('__reactProps$') ||\n key.startsWith('__reactFiber$') ||\n key.startsWith('__reactEventHandlers$')\n ) {\n // Element has React internals, likely has event handlers\n // We can't easily inspect these, so we'll check for common patterns\n const props = (element as unknown as Record<string, unknown>)[key]\n if (props && typeof props === 'object') {\n const propsObj = props as Record<string, unknown>\n if (\n typeof propsObj.onClick === 'function' ||\n typeof propsObj.onMouseDown === 'function' ||\n typeof propsObj.onMouseUp === 'function'\n ) {\n return true\n }\n }\n }\n }\n\n return false\n}\n\n/**\n * Check if element has keyboard event handlers\n */\nfunction hasKeyboardHandler(element: Element): boolean {\n // Check for keyboard event attributes\n for (const event of KEYBOARD_EVENTS) {\n if (element.hasAttribute(event)) {\n return true\n }\n }\n\n const htmlElement = element as HTMLElement\n if (\n typeof htmlElement.onkeydown === 'function' ||\n typeof htmlElement.onkeyup === 'function' ||\n typeof htmlElement.onkeypress === 'function'\n ) {\n return true\n }\n\n // Check React props for keyboard handlers\n const keys = Object.keys(element)\n for (const key of keys) {\n if (key.startsWith('__reactProps$')) {\n const props = (element as unknown as Record<string, unknown>)[key]\n if (props && typeof props === 'object') {\n const propsObj = props as Record<string, unknown>\n if (\n typeof propsObj.onKeyDown === 'function' ||\n typeof propsObj.onKeyUp === 'function' ||\n typeof propsObj.onKeyPress === 'function'\n ) {\n return true\n }\n }\n }\n }\n\n return false\n}\n\n/**\n * Class prefixes to exclude from selectors (devtools overlay classes)\n */\nconst EXCLUDED_CLASS_PREFIXES = ['tsd-a11y-']\n\n/**\n * Filter out devtools-injected classes from class list\n */\nfunction filterClasses(classList: DOMTokenList): Array<string> {\n return Array.from(classList).filter(\n (cls) => !EXCLUDED_CLASS_PREFIXES.some((prefix) => cls.startsWith(prefix)),\n )\n}\n\n/**\n * Get a unique selector for an element\n */\nfunction getSelector(element: Element): string {\n // Try to build a unique selector\n if (element.id) {\n return `#${element.id}`\n }\n\n const tagName = element.tagName.toLowerCase()\n // Filter out devtools overlay classes (tsd-a11y-highlight, etc.)\n const classes = filterClasses(element.classList).join('.')\n const classSelector = classes ? `.${classes}` : ''\n\n // Build path from parent\n const parent = element.parentElement\n if (parent && parent !== document.body) {\n const parentSelector = getSelector(parent)\n const siblings = Array.from(parent.children).filter(\n (el) => el.tagName === element.tagName,\n )\n if (siblings.length > 1) {\n const index = siblings.indexOf(element) + 1\n return `${parentSelector} > ${tagName}${classSelector}:nth-of-type(${index})`\n }\n return `${parentSelector} > ${tagName}${classSelector}`\n }\n\n return `${tagName}${classSelector}`\n}\n\n/**\n * Custom rule: Click handler on non-interactive element\n *\n * This rule detects elements that have click handlers but are not:\n * - Interactive HTML elements (button, a, input, etc.)\n * - Elements with interactive ARIA roles\n * - Elements with tabindex for keyboard access\n */\nfunction checkClickHandlerOnNonInteractive(\n context: Document | Element = document,\n threshold: SeverityThreshold = 'serious',\n): Array<A11yIssue> {\n const issues: Array<A11yIssue> = []\n const timestamp = Date.now()\n\n // Query all elements and check for click handlers\n const allElements = context.querySelectorAll('*')\n\n for (const element of allElements) {\n // Skip devtools elements\n if (isInsideDevtools(element)) {\n continue\n }\n\n // Skip root container elements (e.g., #root, #app)\n // These often have React event delegation attached but no actual click handlers\n if (isRootContainer(element)) {\n continue\n }\n\n // Skip if element is interactive\n if (isInteractiveElement(element) || hasInteractiveRole(element)) {\n continue\n }\n\n // Check if element has click handler\n if (!hasClickHandler(element)) {\n continue\n }\n\n // Element has click handler but is not interactive\n // Check if it at least has keyboard access\n const hasFocus = isFocusable(element)\n const hasKeyboard = hasKeyboardHandler(element)\n\n if (!hasFocus && !hasKeyboard) {\n // Critical: No keyboard access at all\n const selector = getSelector(element)\n issues.push({\n id: `click-handler-no-keyboard-${timestamp}-${issues.length}`,\n ruleId: 'click-handler-on-non-interactive',\n impact: 'serious',\n message:\n 'Element has a click handler but is not keyboard accessible. Add tabindex=\"0\" and keyboard event handlers, or use an interactive element like <button>.',\n help: 'Interactive elements must be keyboard accessible',\n helpUrl:\n 'https://www.w3.org/WAI/WCAG21/Understanding/keyboard-accessible',\n wcagTags: ['wcag211', 'wcag21a'],\n nodes: [\n {\n selector,\n html: element.outerHTML.slice(0, 200),\n },\n ],\n meetsThreshold: meetsThreshold('serious', threshold),\n timestamp,\n })\n } else if (hasFocus && !hasKeyboard) {\n // Moderate: Has tabindex but no keyboard handler\n const selector = getSelector(element)\n issues.push({\n id: `click-handler-no-keyboard-handler-${timestamp}-${issues.length}`,\n ruleId: 'click-handler-on-non-interactive',\n impact: 'moderate',\n message:\n 'Element has a click handler and tabindex but no keyboard event handler. Add onKeyDown/onKeyPress to handle Enter/Space keys.',\n help: 'Interactive elements should respond to keyboard events',\n helpUrl:\n 'https://www.w3.org/WAI/WCAG21/Understanding/keyboard-accessible',\n wcagTags: ['wcag211', 'wcag21a'],\n nodes: [\n {\n selector,\n html: element.outerHTML.slice(0, 200),\n },\n ],\n meetsThreshold: meetsThreshold('moderate', threshold),\n timestamp,\n })\n }\n }\n\n return issues\n}\n\n/**\n * Custom rule: Mouse-only event handlers\n *\n * Detects elements that have mouse event handlers (onmouseover, onmousedown, etc.)\n * without corresponding keyboard event handlers.\n */\nfunction checkMouseOnlyEvents(\n context: Document | Element = document,\n threshold: SeverityThreshold = 'serious',\n): Array<A11yIssue> {\n const issues: Array<A11yIssue> = []\n const timestamp = Date.now()\n // default threshold will be provided by runCustomRules\n // We'll accept threshold by adding a parameter in the function signature\n\n // Build selector for elements with mouse events\n const mouseEventSelectors = MOUSE_ONLY_EVENTS.map(\n (event) => `[${event}]`,\n ).join(', ')\n\n const elements = context.querySelectorAll(mouseEventSelectors)\n\n for (const element of elements) {\n // Skip devtools elements\n if (isInsideDevtools(element)) {\n continue\n }\n\n // Skip interactive elements (they handle keyboard by default)\n if (isInteractiveElement(element)) {\n continue\n }\n\n // Check if element has keyboard handlers\n if (hasKeyboardHandler(element) || isFocusable(element)) {\n continue\n }\n\n const mouseEvents: Array<string> = []\n for (const event of MOUSE_ONLY_EVENTS) {\n if (element.hasAttribute(event)) {\n mouseEvents.push(event)\n }\n }\n\n const selector = getSelector(element)\n issues.push({\n id: `mouse-only-events-${timestamp}-${issues.length}`,\n ruleId: 'mouse-only-event-handlers',\n impact: 'serious',\n message: `Element has mouse-only event handlers (${mouseEvents.join(', ')}) without keyboard equivalents. Ensure functionality is available via keyboard.`,\n help: 'All functionality must be operable through keyboard',\n helpUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/keyboard',\n wcagTags: ['wcag211', 'wcag21a'],\n nodes: [\n {\n selector,\n html: element.outerHTML.slice(0, 200),\n },\n ],\n meetsThreshold: meetsThreshold('serious', threshold),\n timestamp,\n })\n }\n\n return issues\n}\n\n/**\n * Custom rule: Static element with interactive semantics\n *\n * Detects elements like <div> or <span> that have role=\"button\" but lack\n * proper keyboard handling (tabindex and key events).\n */\nfunction checkStaticElementInteraction(\n context: Document | Element = document,\n threshold: SeverityThreshold = 'serious',\n): Array<A11yIssue> {\n const issues: Array<A11yIssue> = []\n const timestamp = Date.now()\n\n // Query elements with interactive roles\n const roleSelectors = Array.from(INTERACTIVE_ROLES)\n .map((role) => `[role=\"${role}\"]`)\n .join(', ')\n\n const elements = context.querySelectorAll(roleSelectors)\n\n for (const element of elements) {\n // Skip devtools elements\n if (isInsideDevtools(element)) {\n continue\n }\n\n // Skip inherently interactive elements\n if (isInteractiveElement(element)) {\n continue\n }\n\n const role = element.getAttribute('role')\n const hasFocus = isFocusable(element)\n const hasKeyboard = hasKeyboardHandler(element)\n\n // Check for missing tabindex\n if (!hasFocus) {\n const selector = getSelector(element)\n issues.push({\n id: `static-element-no-tabindex-${timestamp}-${issues.length}`,\n ruleId: 'static-element-interaction',\n impact: 'serious',\n message: `Element with role=\"${role}\" is not focusable. Add tabindex=\"0\" to make it keyboard accessible.`,\n help: 'Elements with interactive roles must be focusable',\n helpUrl:\n 'https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA4#description',\n wcagTags: ['wcag211', 'wcag21a', 'wcag412'],\n nodes: [\n {\n selector,\n html: element.outerHTML.slice(0, 200),\n },\n ],\n meetsThreshold: meetsThreshold('serious', threshold),\n timestamp,\n })\n }\n\n // Check for missing keyboard handlers (for button-like roles)\n const requiresKeyboardActivation = ['button', 'link', 'menuitem', 'option']\n if (\n role &&\n requiresKeyboardActivation.includes(role) &&\n !hasKeyboard &&\n hasClickHandler(element)\n ) {\n const selector = getSelector(element)\n issues.push({\n id: `static-element-no-keyboard-${timestamp}-${issues.length}`,\n ruleId: 'static-element-interaction',\n impact: 'moderate',\n message: `Element with role=\"${role}\" has click handler but no keyboard handler. Add onKeyDown to handle Enter/Space.`,\n help: 'Elements with button-like roles should respond to Enter and Space keys',\n helpUrl:\n 'https://www.w3.org/WAI/WCAG21/Techniques/aria/ARIA4#description',\n wcagTags: ['wcag211', 'wcag21a'],\n nodes: [\n {\n selector,\n html: element.outerHTML.slice(0, 200),\n },\n ],\n meetsThreshold: meetsThreshold('moderate', threshold),\n timestamp,\n })\n }\n }\n\n return issues\n}\n\n/**\n * Run all enabled custom rules\n */\nexport function runCustomRules(\n context: Document | Element = document,\n config: CustomRulesConfig = {},\n threshold: SeverityThreshold = 'serious',\n): Array<A11yIssue> {\n const {\n clickHandlerOnNonInteractive = true,\n mouseOnlyEventHandlers = true,\n staticElementInteraction = true,\n } = config\n\n const issues: Array<A11yIssue> = []\n\n if (clickHandlerOnNonInteractive) {\n issues.push(...checkClickHandlerOnNonInteractive(context, threshold))\n }\n\n if (mouseOnlyEventHandlers) {\n issues.push(...checkMouseOnlyEvents(context, threshold))\n }\n\n if (staticElementInteraction) {\n issues.push(...checkStaticElementInteraction(context, threshold))\n }\n\n return issues\n}\n\n/**\n * Get list of custom rule metadata (for UI display)\n */\nexport function getCustomRules(): Array<{\n id: string\n description: string\n tags: Array<string>\n}> {\n return [\n {\n id: 'click-handler-on-non-interactive',\n description:\n 'Ensures click handlers are only on keyboard-accessible elements',\n tags: ['custom', 'cat.keyboard', 'wcag21a', 'wcag211'],\n },\n {\n id: 'mouse-only-event-handlers',\n description: 'Ensures mouse event handlers have keyboard equivalents',\n tags: ['custom', 'cat.keyboard', 'wcag21a', 'wcag211'],\n },\n {\n id: 'static-element-interaction',\n description:\n 'Ensures elements with interactive roles are properly keyboard accessible',\n tags: ['custom', 'cat.keyboard', 'cat.aria', 'wcag21a', 'wcag211'],\n },\n ]\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { A11yAuditResult, ExportOptions } from '../types/types.js';
2
+ /**
3
+ * Export audit results to JSON format
4
+ */
5
+ export declare function exportToJson(result: A11yAuditResult, _options?: Partial<ExportOptions>): string;
6
+ /**
7
+ * Export audit results to CSV format
8
+ */
9
+ export declare function exportToCsv(result: A11yAuditResult, _options?: Partial<ExportOptions>): string;
10
+ /**
11
+ * Export audit results and trigger download
12
+ */
13
+ export declare function exportAuditResults(result: A11yAuditResult, options: ExportOptions): void;
14
+ /**
15
+ * Generate a summary report as a formatted string
16
+ */
17
+ export declare function generateSummaryReport(result: A11yAuditResult): string;
@@ -0,0 +1,83 @@
1
+ //#region src/core/utils/export-audit.uitls.ts
2
+ /**
3
+ * Export audit results to JSON format
4
+ */
5
+ function exportToJson(result, _options = {}) {
6
+ const exportData = {
7
+ meta: {
8
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
9
+ url: result.url,
10
+ auditTimestamp: result.timestamp,
11
+ duration: result.duration,
12
+ context: result.context
13
+ },
14
+ summary: result.summary,
15
+ issues: result.issues.map((issue) => ({
16
+ id: issue.id,
17
+ ruleId: issue.ruleId,
18
+ impact: issue.impact,
19
+ message: issue.message,
20
+ help: issue.help,
21
+ helpUrl: issue.helpUrl,
22
+ wcagTags: issue.wcagTags,
23
+ nodes: issue.nodes.map((node) => ({
24
+ selector: node.selector,
25
+ html: node.html,
26
+ failureSummary: node.failureSummary
27
+ }))
28
+ }))
29
+ };
30
+ return JSON.stringify(exportData, null, 2);
31
+ }
32
+ /**
33
+ * Export audit results to CSV format
34
+ */
35
+ function exportToCsv(result, _options = {}) {
36
+ const headers = [
37
+ "Rule ID",
38
+ "Impact",
39
+ "Message",
40
+ "Help URL",
41
+ "WCAG Tags",
42
+ "Selector",
43
+ "HTML"
44
+ ];
45
+ const rows = [];
46
+ for (const issue of result.issues) for (const node of issue.nodes) rows.push([
47
+ issue.ruleId,
48
+ issue.impact,
49
+ issue.message.replace(/"/g, "\"\""),
50
+ issue.helpUrl,
51
+ issue.wcagTags.join("; "),
52
+ node.selector,
53
+ node.html.replace(/"/g, "\"\"")
54
+ ]);
55
+ return [headers.map((h) => `"${h}"`).join(","), ...rows.map((row) => row.map((cell) => `"${cell}"`).join(","))].join("\n");
56
+ }
57
+ /**
58
+ * Download a file with the given content
59
+ */
60
+ function downloadFile(content, filename, mimeType) {
61
+ const blob = new Blob([content], { type: mimeType });
62
+ const url = URL.createObjectURL(blob);
63
+ const link = document.createElement("a");
64
+ link.href = url;
65
+ link.download = filename;
66
+ document.body.appendChild(link);
67
+ link.click();
68
+ document.body.removeChild(link);
69
+ URL.revokeObjectURL(url);
70
+ }
71
+ /**
72
+ * Export audit results and trigger download
73
+ */
74
+ function exportAuditResults(result, options) {
75
+ const { format, filename } = options;
76
+ const defaultFilename = `a11y-audit-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
77
+ if (format === "json") downloadFile(exportToJson(result, options), `${filename || defaultFilename}.json`, "application/json");
78
+ else if (format === "csv") downloadFile(exportToCsv(result, options), `${filename || defaultFilename}.csv`, "text/csv");
79
+ }
80
+ //#endregion
81
+ export { exportAuditResults };
82
+
83
+ //# sourceMappingURL=export-audit.uitls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-audit.uitls.js","names":[],"sources":["../../../../src/core/utils/export-audit.uitls.ts"],"sourcesContent":["import type { A11yAuditResult, ExportOptions } from '../types/types'\n\n/**\n * Export audit results to JSON format\n */\nexport function exportToJson(\n result: A11yAuditResult,\n _options: Partial<ExportOptions> = {},\n): string {\n const exportData = {\n meta: {\n exportedAt: new Date().toISOString(),\n url: result.url,\n auditTimestamp: result.timestamp,\n duration: result.duration,\n context: result.context,\n },\n summary: result.summary,\n issues: result.issues.map((issue) => ({\n id: issue.id,\n ruleId: issue.ruleId,\n impact: issue.impact,\n message: issue.message,\n help: issue.help,\n helpUrl: issue.helpUrl,\n wcagTags: issue.wcagTags,\n nodes: issue.nodes.map((node) => ({\n selector: node.selector,\n html: node.html,\n failureSummary: node.failureSummary,\n })),\n })),\n }\n\n return JSON.stringify(exportData, null, 2)\n}\n\n/**\n * Export audit results to CSV format\n */\nexport function exportToCsv(\n result: A11yAuditResult,\n _options: Partial<ExportOptions> = {},\n): string {\n const headers = [\n 'Rule ID',\n 'Impact',\n 'Message',\n 'Help URL',\n 'WCAG Tags',\n 'Selector',\n 'HTML',\n ]\n\n const rows: Array<Array<string>> = []\n\n for (const issue of result.issues) {\n for (const node of issue.nodes) {\n rows.push([\n issue.ruleId,\n issue.impact,\n issue.message.replace(/\"/g, '\"\"'),\n issue.helpUrl,\n issue.wcagTags.join('; '),\n node.selector,\n node.html.replace(/\"/g, '\"\"'),\n ])\n }\n }\n\n return [\n headers.map((h) => `\"${h}\"`).join(','),\n ...rows.map((row) => row.map((cell) => `\"${cell}\"`).join(',')),\n ].join('\\n')\n}\n\n/**\n * Download a file with the given content\n */\nfunction downloadFile(\n content: string,\n filename: string,\n mimeType: string,\n): void {\n const blob = new Blob([content], { type: mimeType })\n const url = URL.createObjectURL(blob)\n const link = document.createElement('a')\n link.href = url\n link.download = filename\n document.body.appendChild(link)\n link.click()\n document.body.removeChild(link)\n URL.revokeObjectURL(url)\n}\n\n/**\n * Export audit results and trigger download\n */\nexport function exportAuditResults(\n result: A11yAuditResult,\n options: ExportOptions,\n): void {\n const { format, filename } = options\n const timestamp = new Date().toISOString().replace(/[:.]/g, '-')\n const defaultFilename = `a11y-audit-${timestamp}`\n\n if (format === 'json') {\n const content = exportToJson(result, options)\n downloadFile(\n content,\n `${filename || defaultFilename}.json`,\n 'application/json',\n )\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n } else if (format === 'csv') {\n const content = exportToCsv(result, options)\n downloadFile(content, `${filename || defaultFilename}.csv`, 'text/csv')\n }\n}\n\n/**\n * Generate a summary report as a formatted string\n */\nexport function generateSummaryReport(result: A11yAuditResult): string {\n const { summary } = result\n\n const lines = [\n '='.repeat(50),\n 'ACCESSIBILITY AUDIT REPORT',\n '='.repeat(50),\n '',\n `URL: ${result.url}`,\n `Date: ${new Date(result.timestamp).toLocaleString()}`,\n `Duration: ${result.duration.toFixed(2)}ms`,\n '',\n '-'.repeat(50),\n 'SUMMARY',\n '-'.repeat(50),\n '',\n `Total Issues: ${summary.total}`,\n ` - Critical: ${summary.critical}`,\n ` - Serious: ${summary.serious}`,\n ` - Moderate: ${summary.moderate}`,\n ` - Minor: ${summary.minor}`,\n '',\n `Passing Rules: ${summary.passes}`,\n `Incomplete Checks: ${summary.incomplete}`,\n '',\n ]\n\n if (result.issues.length > 0) {\n lines.push('-'.repeat(50))\n lines.push('ISSUES')\n lines.push('-'.repeat(50))\n lines.push('')\n\n const issuesByImpact = {\n critical: result.issues.filter((i) => i.impact === 'critical'),\n serious: result.issues.filter((i) => i.impact === 'serious'),\n moderate: result.issues.filter((i) => i.impact === 'moderate'),\n minor: result.issues.filter((i) => i.impact === 'minor'),\n }\n\n for (const [impact, issues] of Object.entries(issuesByImpact)) {\n if (issues.length > 0) {\n lines.push(`[${impact.toUpperCase()}]`)\n for (const issue of issues) {\n lines.push(` - ${issue.ruleId}: ${issue.message}`)\n lines.push(` Selector: ${issue.nodes[0]?.selector}`)\n lines.push(` Learn more: ${issue.helpUrl}`)\n lines.push('')\n }\n }\n }\n }\n\n lines.push('='.repeat(50))\n\n return lines.join('\\n')\n}\n"],"mappings":";;;;AAKA,SAAgB,aACd,QACA,WAAmC,EAAE,EAC7B;CACR,MAAM,aAAa;EACjB,MAAM;GACJ,6BAAY,IAAI,MAAM,EAAC,aAAa;GACpC,KAAK,OAAO;GACZ,gBAAgB,OAAO;GACvB,UAAU,OAAO;GACjB,SAAS,OAAO;GACjB;EACD,SAAS,OAAO;EAChB,QAAQ,OAAO,OAAO,KAAK,WAAW;GACpC,IAAI,MAAM;GACV,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,UAAU,MAAM;GAChB,OAAO,MAAM,MAAM,KAAK,UAAU;IAChC,UAAU,KAAK;IACf,MAAM,KAAK;IACX,gBAAgB,KAAK;IACtB,EAAE;GACJ,EAAE;EACJ;AAED,QAAO,KAAK,UAAU,YAAY,MAAM,EAAE;;;;;AAM5C,SAAgB,YACd,QACA,WAAmC,EAAE,EAC7B;CACR,MAAM,UAAU;EACd;EACA;EACA;EACA;EACA;EACA;EACA;EACD;CAED,MAAM,OAA6B,EAAE;AAErC,MAAK,MAAM,SAAS,OAAO,OACzB,MAAK,MAAM,QAAQ,MAAM,MACvB,MAAK,KAAK;EACR,MAAM;EACN,MAAM;EACN,MAAM,QAAQ,QAAQ,MAAM,OAAK;EACjC,MAAM;EACN,MAAM,SAAS,KAAK,KAAK;EACzB,KAAK;EACL,KAAK,KAAK,QAAQ,MAAM,OAAK;EAC9B,CAAC;AAIN,QAAO,CACL,QAAQ,KAAK,MAAM,IAAI,EAAE,GAAG,CAAC,KAAK,IAAI,EACtC,GAAG,KAAK,KAAK,QAAQ,IAAI,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAC/D,CAAC,KAAK,KAAK;;;;;AAMd,SAAS,aACP,SACA,UACA,UACM;CACN,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,UAAU,CAAC;CACpD,MAAM,MAAM,IAAI,gBAAgB,KAAK;CACrC,MAAM,OAAO,SAAS,cAAc,IAAI;AACxC,MAAK,OAAO;AACZ,MAAK,WAAW;AAChB,UAAS,KAAK,YAAY,KAAK;AAC/B,MAAK,OAAO;AACZ,UAAS,KAAK,YAAY,KAAK;AAC/B,KAAI,gBAAgB,IAAI;;;;;AAM1B,SAAgB,mBACd,QACA,SACM;CACN,MAAM,EAAE,QAAQ,aAAa;CAE7B,MAAM,kBAAkB,+BADN,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAAI;AAGhE,KAAI,WAAW,OAEb,cADgB,aAAa,QAAQ,QAAQ,EAG3C,GAAG,YAAY,gBAAgB,QAC/B,mBACD;UAEQ,WAAW,MAEpB,cADgB,YAAY,QAAQ,QAAQ,EACtB,GAAG,YAAY,gBAAgB,OAAO,WAAW"}
@@ -0,0 +1,24 @@
1
+ import { A11yIssue, RuleSetPreset, SeverityThreshold } from '../types/types.js';
2
+ export declare function scrollToElement(selector: string): boolean;
3
+ /**
4
+ * Severity levels mapped to numeric values for comparison (higher = more severe)
5
+ */
6
+ export declare const SEVERITY_ORDER: Record<SeverityThreshold, number>;
7
+ export declare const SEVERITY_LABELS: Record<SeverityThreshold, string>;
8
+ export declare const RULE_SET_LABELS: Record<RuleSetPreset, string>;
9
+ /**
10
+ * Highlight a single element with the specified severity
11
+ */
12
+ export declare function highlightElement(selector: string, impact?: SeverityThreshold, options?: {
13
+ showTooltip?: boolean;
14
+ ruleId?: string;
15
+ }): void;
16
+ /**
17
+ * Highlight all elements with issues.
18
+ * Shows all issues per element in the tooltip, using the most severe for highlighting.
19
+ */
20
+ export declare function highlightAllIssues(issues: Array<A11yIssue>): void;
21
+ /**
22
+ * Clear all highlights from the page
23
+ */
24
+ export declare function clearHighlights(): void;