@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,345 @@
1
+ import axe from 'axe-core'
2
+ import {
3
+ getCustomRules as getCustomRulesInternal,
4
+ runCustomRules,
5
+ } from './custom-audit.utils.js'
6
+ import { SEVERITY_ORDER } from './ui.utils.js'
7
+
8
+ // types
9
+ import type { AxeResults, RuleObject, RunOptions } from 'axe-core'
10
+ import type {
11
+ A11yAuditOptions,
12
+ A11yAuditResult,
13
+ A11yIssue,
14
+ A11yNode,
15
+ A11ySummary,
16
+ CustomRulesConfig,
17
+ RuleSetPreset,
18
+ SeverityThreshold,
19
+ } from '../types/types.js'
20
+
21
+ /**
22
+ * Severity levels mapped to numeric values for comparison
23
+ */
24
+ const IMPACT_SEVERITY: Record<SeverityThreshold, number> = {
25
+ critical: 4,
26
+ serious: 3,
27
+ moderate: 2,
28
+ minor: 1,
29
+ }
30
+
31
+ /**
32
+ * Rule set configurations for different presets
33
+ */
34
+ const RULE_SET_CONFIGS: Record<RuleSetPreset, Partial<RunOptions>> = {
35
+ wcag2a: {
36
+ runOnly: {
37
+ type: 'tag',
38
+ values: ['wcag2a'],
39
+ },
40
+ },
41
+ wcag2aa: {
42
+ runOnly: {
43
+ type: 'tag',
44
+ values: ['wcag2a', 'wcag2aa'],
45
+ },
46
+ },
47
+ wcag21aa: {
48
+ runOnly: {
49
+ type: 'tag',
50
+ values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'],
51
+ },
52
+ },
53
+ wcag22aa: {
54
+ runOnly: {
55
+ type: 'tag',
56
+ values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'],
57
+ },
58
+ },
59
+ section508: {
60
+ runOnly: {
61
+ type: 'tag',
62
+ values: ['section508'],
63
+ },
64
+ },
65
+ 'best-practice': {
66
+ runOnly: {
67
+ type: 'tag',
68
+ values: ['best-practice'],
69
+ },
70
+ },
71
+ all: {
72
+ // Run all rules
73
+ },
74
+ }
75
+
76
+ /**
77
+ * Check if an impact level meets or exceeds the threshold
78
+ */
79
+ export function meetsThreshold(
80
+ impact: SeverityThreshold | null | undefined,
81
+ threshold: SeverityThreshold,
82
+ ): boolean {
83
+ if (!impact) return false
84
+ return IMPACT_SEVERITY[impact] >= IMPACT_SEVERITY[threshold]
85
+ }
86
+
87
+ /**
88
+ * Convert axe-core results to our issue format
89
+ */
90
+ function convertToIssues(
91
+ results: AxeResults,
92
+ threshold: SeverityThreshold,
93
+ ): Array<A11yIssue> {
94
+ const issues: Array<A11yIssue> = []
95
+
96
+ for (const violation of results.violations) {
97
+ const impact = violation.impact as SeverityThreshold | undefined
98
+
99
+ for (let i = 0; i < violation.nodes.length; i++) {
100
+ const node = violation.nodes[i]!
101
+ const selector = node.target.join(', ')
102
+
103
+ const a11yNode: A11yNode = {
104
+ selector,
105
+ html: node.html,
106
+ xpath: node.xpath?.join(' > '),
107
+ failureSummary: node.failureSummary,
108
+ }
109
+
110
+ issues.push({
111
+ id: `${violation.id}-${i}-${Date.now()}`,
112
+ ruleId: violation.id,
113
+ impact: impact || 'minor',
114
+ message: node.failureSummary || violation.description,
115
+ help: violation.help,
116
+ helpUrl: violation.helpUrl,
117
+ wcagTags: violation.tags.filter(
118
+ (tag) => tag.startsWith('wcag') || tag.startsWith('section508'),
119
+ ),
120
+ nodes: [a11yNode],
121
+ meetsThreshold: meetsThreshold(impact, threshold),
122
+ timestamp: Date.now(),
123
+ })
124
+ }
125
+ }
126
+
127
+ return issues
128
+ }
129
+
130
+ /**
131
+ * Create summary statistics from issues array
132
+ * Used when combining axe-core results with custom rule results
133
+ */
134
+ function createSummary(
135
+ axeResults: AxeResults,
136
+ issues: Array<A11yIssue>,
137
+ ): A11ySummary {
138
+ const summary: A11ySummary = {
139
+ total: issues.length,
140
+ critical: 0,
141
+ serious: 0,
142
+ moderate: 0,
143
+ minor: 0,
144
+ passes: axeResults.passes.length,
145
+ incomplete: axeResults.incomplete.length,
146
+ }
147
+
148
+ for (const issue of issues) {
149
+ const impact = issue.impact
150
+ if (impact === 'critical') summary.critical++
151
+ else if (impact === 'serious') summary.serious++
152
+ else if (impact === 'moderate') summary.moderate++
153
+ else {
154
+ summary.minor++
155
+ }
156
+ }
157
+
158
+ return summary
159
+ }
160
+
161
+ /**
162
+ * Get the context description for logging
163
+ */
164
+ function getContextDescription(context: Document | Element | string): string {
165
+ if (typeof context === 'string') {
166
+ return context
167
+ }
168
+ if (context instanceof Document) {
169
+ return 'document'
170
+ }
171
+ if (context instanceof Element) {
172
+ return context.tagName.toLowerCase() + (context.id ? `#${context.id}` : '')
173
+ }
174
+ return 'unknown'
175
+ }
176
+
177
+ /**
178
+ * Default selectors to exclude from auditing (devtools panels, overlays, etc.)
179
+ */
180
+ const DEFAULT_EXCLUDE_SELECTORS = [
181
+ // TanStack Devtools root container
182
+ '[data-testid="tanstack_devtools"]',
183
+ // A11y overlay elements
184
+ '[data-a11y-overlay]',
185
+ // Common devtools patterns
186
+ '[data-devtools]',
187
+ '[data-devtools-panel]',
188
+ ]
189
+
190
+ /**
191
+ * Run an accessibility audit using axe-core
192
+ */
193
+ export async function runAudit(
194
+ options: A11yAuditOptions = {},
195
+ ): Promise<A11yAuditResult> {
196
+ const {
197
+ threshold = 'serious',
198
+ context = document,
199
+ ruleSet = 'wcag21aa',
200
+ enabledRules,
201
+ disabledRules,
202
+ exclude = [],
203
+ customRules = {},
204
+ } = options
205
+
206
+ // Merge user exclusions with default devtools exclusions
207
+ const allExclusions = [...DEFAULT_EXCLUDE_SELECTORS, ...exclude]
208
+
209
+ const startTime = performance.now()
210
+ const contextDescription = getContextDescription(context)
211
+
212
+ try {
213
+ // Build axe-core options
214
+ const axeOptions: RunOptions = {
215
+ resultTypes: ['violations', 'passes', 'incomplete'],
216
+ ...RULE_SET_CONFIGS[ruleSet],
217
+ }
218
+
219
+ // Handle specific rule configurations
220
+ if (enabledRules && enabledRules.length > 0) {
221
+ axeOptions.runOnly = {
222
+ type: 'rule',
223
+ values: enabledRules,
224
+ }
225
+ }
226
+
227
+ // Build rules configuration for disabled rules
228
+ if (disabledRules && disabledRules.length > 0) {
229
+ const rules: RuleObject = {}
230
+ for (const ruleId of disabledRules) {
231
+ rules[ruleId] = { enabled: false }
232
+ }
233
+ axeOptions.rules = rules
234
+ }
235
+
236
+ // Determine the context to audit
237
+ let auditContext: axe.ElementContext = context as axe.ElementContext
238
+
239
+ // Add exclusions if specified (always include devtools exclusions)
240
+ if (allExclusions.length > 0) {
241
+ auditContext = {
242
+ include: [auditContext as Element],
243
+ exclude: allExclusions.map((sel) => [sel]),
244
+ } as axe.ElementContext
245
+ }
246
+
247
+ // Run the axe-core audit
248
+ const results = await axe.run(auditContext, axeOptions)
249
+
250
+ // Convert axe-core results to our format
251
+ const axeIssues = convertToIssues(results, threshold)
252
+
253
+ // Run custom rules (if not all disabled)
254
+ const customRulesConfig: CustomRulesConfig = {
255
+ clickHandlerOnNonInteractive:
256
+ customRules.clickHandlerOnNonInteractive !== false &&
257
+ !disabledRules?.includes('click-handler-on-non-interactive'),
258
+ mouseOnlyEventHandlers:
259
+ customRules.mouseOnlyEventHandlers !== false &&
260
+ !disabledRules?.includes('mouse-only-event-handlers'),
261
+ staticElementInteraction:
262
+ customRules.staticElementInteraction !== false &&
263
+ !disabledRules?.includes('static-element-interaction'),
264
+ }
265
+
266
+ const contextElement =
267
+ typeof context === 'string'
268
+ ? document.querySelector(context) || document
269
+ : context
270
+
271
+ const customIssues = runCustomRules(
272
+ contextElement,
273
+ customRulesConfig,
274
+ threshold,
275
+ )
276
+
277
+ // Merge all issues
278
+ const allIssues = [...axeIssues, ...customIssues]
279
+
280
+ const duration = performance.now() - startTime
281
+
282
+ // Create summary from combined issues
283
+ const summary = createSummary(results, allIssues)
284
+
285
+ return {
286
+ issues: allIssues,
287
+ summary,
288
+ timestamp: Date.now(),
289
+ url: typeof window !== 'undefined' ? window.location.href : '',
290
+ context: contextDescription,
291
+ duration,
292
+ }
293
+ } catch (error) {
294
+ const duration = performance.now() - startTime
295
+ console.error('[A11y Audit] Error running axe-core:', error)
296
+
297
+ return {
298
+ issues: [],
299
+ summary: {
300
+ total: 0,
301
+ critical: 0,
302
+ serious: 0,
303
+ moderate: 0,
304
+ minor: 0,
305
+ passes: 0,
306
+ incomplete: 0,
307
+ },
308
+ timestamp: Date.now(),
309
+ url: typeof window !== 'undefined' ? window.location.href : '',
310
+ context: contextDescription,
311
+ duration,
312
+ }
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Get a list of all available axe-core rules plus custom rules
318
+ */
319
+ export function getAvailableRules(): Array<{
320
+ id: string
321
+ description: string
322
+ tags: Array<string>
323
+ }> {
324
+ // Get axe-core rules
325
+ const axeRules = axe.getRules().map((rule) => ({
326
+ id: rule.ruleId,
327
+ description: rule.description,
328
+ tags: rule.tags,
329
+ }))
330
+
331
+ // Get custom rules
332
+ const customRules = getCustomRulesInternal()
333
+
334
+ return [...axeRules, ...customRules]
335
+ }
336
+
337
+ export const IMPACTS = ['critical', 'serious', 'moderate', 'minor'] as const
338
+
339
+ export const filterIssuesAboveThreshold = (
340
+ issues: A11yAuditResult['issues'],
341
+ threshold: SeverityThreshold,
342
+ ) =>
343
+ issues.filter(
344
+ (issue) => SEVERITY_ORDER[issue.impact] >= SEVERITY_ORDER[threshold],
345
+ )
@@ -0,0 +1,68 @@
1
+ import type { A11yPluginOptions } from '../types/types'
2
+
3
+ const STORAGE_KEY = 'tanstack-devtools-a11y-config'
4
+
5
+ /**
6
+ * Default plugin configuration
7
+ */
8
+ export const DEFAULT_CONFIG: Required<A11yPluginOptions> = {
9
+ threshold: 'serious',
10
+ ruleSet: 'wcag21aa',
11
+ showOverlays: true,
12
+ persistSettings: true,
13
+ disabledRules: [],
14
+ }
15
+
16
+ /**
17
+ * Load configuration from localStorage
18
+ */
19
+ export function loadConfig(): Required<A11yPluginOptions> {
20
+ if (typeof localStorage === 'undefined') {
21
+ return DEFAULT_CONFIG
22
+ }
23
+
24
+ try {
25
+ const stored = localStorage.getItem(STORAGE_KEY)
26
+ if (stored) {
27
+ const parsed = JSON.parse(stored) as Partial<A11yPluginOptions>
28
+ return { ...DEFAULT_CONFIG, ...parsed }
29
+ }
30
+ } catch (error) {
31
+ console.warn(
32
+ '[A11y Config] Failed to load config from localStorage:',
33
+ error,
34
+ )
35
+ }
36
+
37
+ return DEFAULT_CONFIG
38
+ }
39
+
40
+ /**
41
+ * Save configuration to localStorage
42
+ */
43
+ export function saveConfig(config: Partial<A11yPluginOptions>): void {
44
+ if (typeof localStorage === 'undefined') {
45
+ return
46
+ }
47
+
48
+ try {
49
+ const current = loadConfig()
50
+ const updated = { ...current, ...config }
51
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(updated))
52
+ } catch (error) {
53
+ console.warn('[A11y Config] Failed to save config to localStorage:', error)
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Merge user options with defaults
59
+ */
60
+ export function mergeConfig(
61
+ options: A11yPluginOptions = {},
62
+ ): Required<A11yPluginOptions> {
63
+ if (options.persistSettings !== false) {
64
+ const saved = loadConfig()
65
+ return { ...saved, ...options }
66
+ }
67
+ return { ...DEFAULT_CONFIG, ...options }
68
+ }