eslint-plugin-a11y 1.0.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.mjs ADDED
@@ -0,0 +1,1891 @@
1
+ // src/core/aria-spec.ts
2
+ var DEPRECATED_ARIA = {
3
+ roles: [],
4
+ properties: ["aria-dropeffect", "aria-grabbed"],
5
+ states: ["aria-grabbed"]
6
+ };
7
+ var ARIA_IN_HTML = {
8
+ // Attributes that are global but discouraged on certain elements
9
+ discouraged: {
10
+ 'input[type="text"]': ["aria-label"],
11
+ // Should use <label> instead
12
+ 'input[type="email"]': ["aria-label"],
13
+ 'input[type="password"]': ["aria-label"],
14
+ 'input[type="number"]': ["aria-label"],
15
+ 'input[type="tel"]': ["aria-label"],
16
+ 'input[type="url"]': ["aria-label"],
17
+ 'input[type="search"]': ["aria-label"],
18
+ "button": ["role"],
19
+ // Redundant if role="button"
20
+ "a": ["role"],
21
+ // Redundant if role="link"
22
+ "h1": ["role"],
23
+ // Redundant if role="heading"
24
+ "h2": ["role"],
25
+ "h3": ["role"],
26
+ "h4": ["role"],
27
+ "h5": ["role"],
28
+ "h6": ["role"],
29
+ "img": ["role"],
30
+ // Redundant if role="img"
31
+ "ul": ["role"],
32
+ // Redundant if role="list"
33
+ "ol": ["role"],
34
+ "li": ["role"],
35
+ // Redundant if role="listitem"
36
+ "nav": ["role"],
37
+ // Redundant if role="navigation"
38
+ "main": ["role"],
39
+ // Redundant if role="main"
40
+ "article": ["role"],
41
+ // Redundant if role="article"
42
+ "section": ["role"],
43
+ // Redundant if role="section"
44
+ "header": ["role"],
45
+ // Redundant if role="banner"
46
+ "footer": ["role"],
47
+ // Redundant if role="contentinfo"
48
+ "aside": ["role"],
49
+ // Redundant if role="complementary"
50
+ "form": ["role"],
51
+ // Redundant if role="form"
52
+ "dialog": ["role"]
53
+ // Redundant if role="dialog"
54
+ },
55
+ // Native elements that already have implicit roles
56
+ implicitRoles: {
57
+ "button": "button",
58
+ "a": "link",
59
+ "img": "img",
60
+ "h1": "heading",
61
+ "h2": "heading",
62
+ "h3": "heading",
63
+ "h4": "heading",
64
+ "h5": "heading",
65
+ "h6": "heading",
66
+ "ul": "list",
67
+ "ol": "list",
68
+ "li": "listitem",
69
+ "nav": "navigation",
70
+ "main": "main",
71
+ "article": "article",
72
+ "section": "region",
73
+ "header": "banner",
74
+ "footer": "contentinfo",
75
+ "aside": "complementary",
76
+ "form": "form",
77
+ "dialog": "dialog",
78
+ 'input[type="button"]': "button",
79
+ 'input[type="submit"]': "button",
80
+ 'input[type="reset"]': "button",
81
+ 'input[type="checkbox"]': "checkbox",
82
+ 'input[type="radio"]': "radio",
83
+ 'input[type="text"]': "textbox",
84
+ 'input[type="email"]': "textbox",
85
+ 'input[type="password"]': "textbox",
86
+ 'input[type="number"]': "spinbutton",
87
+ 'input[type="search"]': "searchbox",
88
+ "select": "combobox",
89
+ "textarea": "textbox"
90
+ }
91
+ };
92
+ var ARIA_ROLES = {
93
+ // Widget Roles
94
+ "button": {
95
+ requiredProperties: [],
96
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-pressed", "aria-expanded", "aria-disabled", "aria-haspopup"],
97
+ allowedOn: ["button", "a", "div", "span", "input"],
98
+ deprecated: false,
99
+ abstract: false
100
+ },
101
+ "checkbox": {
102
+ requiredProperties: [],
103
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-checked", "aria-required", "aria-disabled"],
104
+ allowedOn: ["input", "div", "span"],
105
+ deprecated: false,
106
+ abstract: false
107
+ },
108
+ "radio": {
109
+ requiredProperties: [],
110
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-checked", "aria-required", "aria-disabled"],
111
+ allowedOn: ["input", "div", "span"],
112
+ deprecated: false,
113
+ abstract: false
114
+ },
115
+ "switch": {
116
+ requiredProperties: [],
117
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-checked", "aria-required", "aria-disabled"],
118
+ allowedOn: ["button", "div", "span"],
119
+ deprecated: false,
120
+ abstract: false
121
+ },
122
+ "tab": {
123
+ requiredProperties: [],
124
+ allowedProperties: ["aria-selected", "aria-controls", "aria-labelledby", "aria-disabled"],
125
+ allowedOn: ["button", "a", "div", "span"],
126
+ requiredContext: "tablist",
127
+ deprecated: false,
128
+ abstract: false
129
+ },
130
+ "tabpanel": {
131
+ requiredProperties: [],
132
+ allowedProperties: ["aria-labelledby"],
133
+ allowedOn: ["div", "section"],
134
+ deprecated: false,
135
+ abstract: false
136
+ },
137
+ "combobox": {
138
+ requiredProperties: [],
139
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-expanded", "aria-controls", "aria-autocomplete", "aria-required"],
140
+ allowedOn: ["input", "select", "div", "span"],
141
+ deprecated: false,
142
+ abstract: false
143
+ },
144
+ "slider": {
145
+ requiredProperties: [],
146
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext", "aria-orientation"],
147
+ allowedOn: ["input", "div", "span"],
148
+ deprecated: false,
149
+ abstract: false
150
+ },
151
+ "spinbutton": {
152
+ requiredProperties: [],
153
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext", "aria-required"],
154
+ allowedOn: ["input", "div", "span"],
155
+ deprecated: false,
156
+ abstract: false
157
+ },
158
+ "textbox": {
159
+ requiredProperties: [],
160
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-multiline", "aria-required", "aria-readonly", "aria-invalid", "aria-autocomplete"],
161
+ allowedOn: ["input", "textarea", "div", "span"],
162
+ deprecated: false,
163
+ abstract: false
164
+ },
165
+ "searchbox": {
166
+ requiredProperties: [],
167
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-required", "aria-autocomplete"],
168
+ allowedOn: ["input", "div", "span"],
169
+ deprecated: false,
170
+ abstract: false
171
+ },
172
+ "menuitem": {
173
+ requiredProperties: [],
174
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-disabled"],
175
+ allowedOn: ["li", "div", "span"],
176
+ requiredContext: ["menu", "menubar"],
177
+ deprecated: false,
178
+ abstract: false
179
+ },
180
+ "menuitemcheckbox": {
181
+ requiredProperties: [],
182
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-checked", "aria-disabled"],
183
+ allowedOn: ["li", "div", "span"],
184
+ requiredContext: ["menu", "menubar"],
185
+ deprecated: false,
186
+ abstract: false
187
+ },
188
+ "menuitemradio": {
189
+ requiredProperties: [],
190
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-checked", "aria-disabled"],
191
+ allowedOn: ["li", "div", "span"],
192
+ requiredContext: ["menu", "menubar"],
193
+ deprecated: false,
194
+ abstract: false
195
+ },
196
+ "option": {
197
+ requiredProperties: [],
198
+ allowedProperties: ["aria-selected", "aria-checked", "aria-disabled"],
199
+ allowedOn: ["li", "div", "span"],
200
+ requiredContext: "listbox",
201
+ deprecated: false,
202
+ abstract: false
203
+ },
204
+ "treeitem": {
205
+ requiredProperties: [],
206
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-expanded", "aria-selected", "aria-level"],
207
+ allowedOn: ["li", "div", "span"],
208
+ requiredContext: "tree",
209
+ deprecated: false,
210
+ abstract: false
211
+ },
212
+ // Composite Widget Roles
213
+ "menu": {
214
+ requiredProperties: [],
215
+ allowedProperties: ["aria-label", "aria-labelledby"],
216
+ allowedOn: ["ul", "menu", "div", "span"],
217
+ deprecated: false,
218
+ abstract: false
219
+ },
220
+ "menubar": {
221
+ requiredProperties: [],
222
+ allowedProperties: ["aria-label", "aria-labelledby"],
223
+ allowedOn: ["ul", "menu", "div", "span"],
224
+ deprecated: false,
225
+ abstract: false
226
+ },
227
+ "tablist": {
228
+ requiredProperties: [],
229
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-orientation"],
230
+ allowedOn: ["ul", "div", "span"],
231
+ deprecated: false,
232
+ abstract: false
233
+ },
234
+ "tree": {
235
+ requiredProperties: [],
236
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-required"],
237
+ allowedOn: ["ul", "div", "span"],
238
+ deprecated: false,
239
+ abstract: false
240
+ },
241
+ "treegrid": {
242
+ requiredProperties: [],
243
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-expanded", "aria-level"],
244
+ allowedOn: ["table", "div", "span"],
245
+ deprecated: false,
246
+ abstract: false
247
+ },
248
+ "grid": {
249
+ requiredProperties: [],
250
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-expanded", "aria-level"],
251
+ allowedOn: ["table", "div", "span"],
252
+ deprecated: false,
253
+ abstract: false
254
+ },
255
+ "listbox": {
256
+ requiredProperties: [],
257
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-required", "aria-multiselectable"],
258
+ allowedOn: ["ul", "select", "div", "span"],
259
+ deprecated: false,
260
+ abstract: false
261
+ },
262
+ // Document Structure Roles
263
+ "article": {
264
+ requiredProperties: [],
265
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-expanded"],
266
+ allowedOn: ["article", "div", "section"],
267
+ deprecated: false,
268
+ abstract: false
269
+ },
270
+ "section": {
271
+ requiredProperties: [],
272
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-expanded"],
273
+ allowedOn: ["section", "div"],
274
+ deprecated: false,
275
+ abstract: false
276
+ },
277
+ "navigation": {
278
+ requiredProperties: [],
279
+ allowedProperties: ["aria-label", "aria-labelledby"],
280
+ allowedOn: ["nav", "div"],
281
+ deprecated: false,
282
+ abstract: false
283
+ },
284
+ "main": {
285
+ requiredProperties: [],
286
+ allowedProperties: ["aria-label", "aria-labelledby"],
287
+ allowedOn: ["main", "div"],
288
+ deprecated: false,
289
+ abstract: false
290
+ },
291
+ "complementary": {
292
+ requiredProperties: [],
293
+ allowedProperties: ["aria-label", "aria-labelledby"],
294
+ allowedOn: ["aside", "div"],
295
+ deprecated: false,
296
+ abstract: false
297
+ },
298
+ "contentinfo": {
299
+ requiredProperties: [],
300
+ allowedProperties: ["aria-label", "aria-labelledby"],
301
+ allowedOn: ["footer", "div"],
302
+ deprecated: false,
303
+ abstract: false
304
+ },
305
+ "search": {
306
+ requiredProperties: [],
307
+ allowedProperties: ["aria-label", "aria-labelledby"],
308
+ allowedOn: ["form", "div", "section"],
309
+ deprecated: false,
310
+ abstract: false
311
+ },
312
+ "form": {
313
+ requiredProperties: [],
314
+ allowedProperties: ["aria-label", "aria-labelledby"],
315
+ allowedOn: ["form", "div"],
316
+ deprecated: false,
317
+ abstract: false
318
+ },
319
+ "region": {
320
+ requiredProperties: [],
321
+ allowedProperties: ["aria-label", "aria-labelledby"],
322
+ allowedOn: ["section", "div"],
323
+ deprecated: false,
324
+ abstract: false
325
+ },
326
+ // Landmark Roles
327
+ "banner": {
328
+ requiredProperties: [],
329
+ allowedProperties: ["aria-label", "aria-labelledby"],
330
+ allowedOn: ["header", "div"],
331
+ deprecated: false,
332
+ abstract: false
333
+ },
334
+ "application": {
335
+ requiredProperties: [],
336
+ allowedProperties: ["aria-label", "aria-labelledby"],
337
+ allowedOn: ["div", "span"],
338
+ deprecated: false,
339
+ abstract: false
340
+ },
341
+ // Live Region Roles
342
+ "alert": {
343
+ requiredProperties: [],
344
+ allowedProperties: ["aria-label", "aria-labelledby"],
345
+ allowedOn: ["div", "span"],
346
+ deprecated: false,
347
+ abstract: false
348
+ },
349
+ "status": {
350
+ requiredProperties: [],
351
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-live", "aria-atomic"],
352
+ allowedOn: ["div", "span", "output"],
353
+ deprecated: false,
354
+ abstract: false
355
+ },
356
+ "log": {
357
+ requiredProperties: [],
358
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-live", "aria-atomic"],
359
+ allowedOn: ["div", "span"],
360
+ deprecated: false,
361
+ abstract: false
362
+ },
363
+ "marquee": {
364
+ requiredProperties: [],
365
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-live", "aria-atomic"],
366
+ allowedOn: ["div", "span"],
367
+ deprecated: false,
368
+ abstract: false
369
+ },
370
+ "timer": {
371
+ requiredProperties: [],
372
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-live", "aria-atomic"],
373
+ allowedOn: ["div", "span"],
374
+ deprecated: false,
375
+ abstract: false
376
+ },
377
+ // Window Roles
378
+ "dialog": {
379
+ requiredProperties: ["aria-label", "aria-labelledby"],
380
+ // One required
381
+ allowedProperties: ["aria-modal", "aria-describedby"],
382
+ allowedOn: ["dialog", "div"],
383
+ deprecated: false,
384
+ abstract: false
385
+ },
386
+ "alertdialog": {
387
+ requiredProperties: ["aria-label", "aria-labelledby"],
388
+ // One required
389
+ allowedProperties: ["aria-modal", "aria-describedby"],
390
+ allowedOn: ["dialog", "div"],
391
+ deprecated: false,
392
+ abstract: false
393
+ },
394
+ // High Priority Additions
395
+ "link": {
396
+ requiredProperties: [],
397
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-expanded"],
398
+ allowedOn: ["a", "div", "span"],
399
+ deprecated: false,
400
+ abstract: false
401
+ },
402
+ "heading": {
403
+ requiredProperties: [],
404
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-level"],
405
+ allowedOn: ["h1", "h2", "h3", "h4", "h5", "h6", "div", "span"],
406
+ deprecated: false,
407
+ abstract: false
408
+ },
409
+ "img": {
410
+ requiredProperties: [],
411
+ allowedProperties: ["aria-label", "aria-labelledby"],
412
+ allowedOn: ["img", "div", "span"],
413
+ deprecated: false,
414
+ abstract: false
415
+ },
416
+ "progressbar": {
417
+ requiredProperties: [],
418
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext"],
419
+ allowedOn: ["progress", "div", "span"],
420
+ deprecated: false,
421
+ abstract: false
422
+ },
423
+ "meter": {
424
+ requiredProperties: [],
425
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-valuemin", "aria-valuemax", "aria-valuenow", "aria-valuetext"],
426
+ allowedOn: ["meter", "div", "span"],
427
+ deprecated: false,
428
+ abstract: false
429
+ },
430
+ "separator": {
431
+ requiredProperties: [],
432
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-orientation"],
433
+ allowedOn: ["hr", "div", "span"],
434
+ deprecated: false,
435
+ abstract: false
436
+ },
437
+ "toolbar": {
438
+ requiredProperties: [],
439
+ allowedProperties: ["aria-label", "aria-labelledby", "aria-orientation"],
440
+ allowedOn: ["div", "span"],
441
+ deprecated: false,
442
+ abstract: false
443
+ },
444
+ "tooltip": {
445
+ requiredProperties: [],
446
+ allowedProperties: ["aria-label", "aria-labelledby"],
447
+ allowedOn: ["div", "span"],
448
+ deprecated: false,
449
+ abstract: false
450
+ },
451
+ "none": {
452
+ requiredProperties: [],
453
+ allowedProperties: [],
454
+ allowedOn: ["*"],
455
+ deprecated: false,
456
+ abstract: false
457
+ },
458
+ "presentation": {
459
+ requiredProperties: [],
460
+ allowedProperties: [],
461
+ allowedOn: ["*"],
462
+ deprecated: false,
463
+ abstract: false
464
+ }
465
+ };
466
+ var ARIA_PROPERTIES = {
467
+ // Labeling Properties
468
+ "aria-label": {
469
+ type: "string",
470
+ required: false,
471
+ allowedOn: ["*"],
472
+ deprecated: false
473
+ },
474
+ "aria-labelledby": {
475
+ type: "idrefs",
476
+ required: false,
477
+ allowedOn: ["*"],
478
+ deprecated: false
479
+ },
480
+ "aria-describedby": {
481
+ type: "idrefs",
482
+ required: false,
483
+ allowedOn: ["*"],
484
+ deprecated: false
485
+ },
486
+ // Relationship Properties
487
+ "aria-owns": {
488
+ type: "idrefs",
489
+ required: false,
490
+ allowedOn: ["*"],
491
+ deprecated: false
492
+ },
493
+ "aria-controls": {
494
+ type: "idrefs",
495
+ required: false,
496
+ allowedOn: ["*"],
497
+ deprecated: false
498
+ },
499
+ "aria-flowto": {
500
+ type: "idrefs",
501
+ required: false,
502
+ allowedOn: ["*"],
503
+ deprecated: false
504
+ },
505
+ "aria-activedescendant": {
506
+ type: "idref",
507
+ required: false,
508
+ allowedOn: ["*"],
509
+ deprecated: false
510
+ },
511
+ // Live Region Properties
512
+ "aria-live": {
513
+ type: "enum",
514
+ enumValues: ["off", "polite", "assertive"],
515
+ required: false,
516
+ allowedOn: ["*"],
517
+ deprecated: false
518
+ },
519
+ "aria-atomic": {
520
+ type: "boolean",
521
+ required: false,
522
+ allowedOn: ["*"],
523
+ deprecated: false
524
+ },
525
+ "aria-relevant": {
526
+ type: "enum",
527
+ enumValues: ["additions", "removals", "text", "all"],
528
+ required: false,
529
+ allowedOn: ["*"],
530
+ deprecated: false
531
+ },
532
+ "aria-busy": {
533
+ type: "boolean",
534
+ required: false,
535
+ allowedOn: ["*"],
536
+ deprecated: false
537
+ },
538
+ // Drag and Drop Properties (Deprecated)
539
+ "aria-dropeffect": {
540
+ type: "enum",
541
+ enumValues: ["copy", "move", "link", "execute", "popup", "none"],
542
+ required: false,
543
+ allowedOn: ["*"],
544
+ deprecated: true
545
+ },
546
+ "aria-grabbed": {
547
+ type: "enum",
548
+ enumValues: ["true", "false"],
549
+ required: false,
550
+ allowedOn: ["*"],
551
+ deprecated: true
552
+ },
553
+ // Global Properties
554
+ "aria-hidden": {
555
+ type: "boolean",
556
+ required: false,
557
+ allowedOn: ["*"],
558
+ deprecated: false
559
+ },
560
+ "aria-invalid": {
561
+ type: "enum",
562
+ enumValues: ["true", "false", "grammar", "spelling"],
563
+ required: false,
564
+ allowedOn: ["*"],
565
+ deprecated: false
566
+ },
567
+ "aria-required": {
568
+ type: "boolean",
569
+ required: false,
570
+ allowedOn: ["*"],
571
+ deprecated: false
572
+ },
573
+ "aria-readonly": {
574
+ type: "boolean",
575
+ required: false,
576
+ allowedOn: ["*"],
577
+ deprecated: false
578
+ },
579
+ "aria-disabled": {
580
+ type: "boolean",
581
+ required: false,
582
+ allowedOn: ["*"],
583
+ deprecated: false
584
+ },
585
+ // Widget Properties
586
+ "aria-autocomplete": {
587
+ type: "enum",
588
+ enumValues: ["none", "inline", "list", "both"],
589
+ required: false,
590
+ allowedOn: ["input", "textarea", "div", "span"],
591
+ deprecated: false
592
+ },
593
+ "aria-checked": {
594
+ type: "tristate",
595
+ required: false,
596
+ allowedOn: ["input", "div", "span"],
597
+ deprecated: false
598
+ },
599
+ "aria-expanded": {
600
+ type: "tristate",
601
+ required: false,
602
+ allowedOn: ["*"],
603
+ deprecated: false
604
+ },
605
+ "aria-haspopup": {
606
+ type: "enum",
607
+ enumValues: ["true", "false", "menu", "listbox", "tree", "grid", "dialog"],
608
+ required: false,
609
+ allowedOn: ["*"],
610
+ deprecated: false
611
+ },
612
+ "aria-level": {
613
+ type: "integer",
614
+ required: false,
615
+ allowedOn: ["*"],
616
+ deprecated: false
617
+ },
618
+ "aria-modal": {
619
+ type: "boolean",
620
+ required: false,
621
+ allowedOn: ["dialog", "div"],
622
+ deprecated: false
623
+ },
624
+ "aria-multiline": {
625
+ type: "boolean",
626
+ required: false,
627
+ allowedOn: ["textarea", "div", "span"],
628
+ deprecated: false
629
+ },
630
+ "aria-multiselectable": {
631
+ type: "boolean",
632
+ required: false,
633
+ allowedOn: ["select", "div", "span"],
634
+ deprecated: false
635
+ },
636
+ "aria-orientation": {
637
+ type: "enum",
638
+ enumValues: ["horizontal", "vertical"],
639
+ required: false,
640
+ allowedOn: ["*"],
641
+ deprecated: false
642
+ },
643
+ "aria-pressed": {
644
+ type: "tristate",
645
+ required: false,
646
+ allowedOn: ["button", "div", "span"],
647
+ deprecated: false
648
+ },
649
+ "aria-selected": {
650
+ type: "boolean",
651
+ required: false,
652
+ allowedOn: ["option", "tab", "treeitem", "div", "span"],
653
+ deprecated: false
654
+ },
655
+ "aria-sort": {
656
+ type: "enum",
657
+ enumValues: ["ascending", "descending", "none", "other"],
658
+ required: false,
659
+ allowedOn: ["th", "td", "div", "span"],
660
+ deprecated: false
661
+ },
662
+ // Range Properties
663
+ "aria-valuemax": {
664
+ type: "number",
665
+ required: false,
666
+ allowedOn: ["input", "progress", "meter", "div", "span"],
667
+ deprecated: false
668
+ },
669
+ "aria-valuemin": {
670
+ type: "number",
671
+ required: false,
672
+ allowedOn: ["input", "progress", "meter", "div", "span"],
673
+ deprecated: false
674
+ },
675
+ "aria-valuenow": {
676
+ type: "number",
677
+ required: false,
678
+ allowedOn: ["input", "progress", "meter", "div", "span"],
679
+ deprecated: false
680
+ },
681
+ "aria-valuetext": {
682
+ type: "string",
683
+ required: false,
684
+ allowedOn: ["input", "progress", "meter", "div", "span"],
685
+ deprecated: false
686
+ },
687
+ // High Priority Additions
688
+ "aria-current": {
689
+ type: "enum",
690
+ enumValues: ["page", "step", "location", "date", "time", "true", "false"],
691
+ required: false,
692
+ allowedOn: ["*"],
693
+ deprecated: false
694
+ },
695
+ "aria-keyshortcuts": {
696
+ type: "string",
697
+ required: false,
698
+ allowedOn: ["*"],
699
+ deprecated: false
700
+ },
701
+ "aria-roledescription": {
702
+ type: "string",
703
+ required: false,
704
+ allowedOn: ["*"],
705
+ deprecated: false
706
+ },
707
+ "aria-posinset": {
708
+ type: "integer",
709
+ required: false,
710
+ allowedOn: ["*"],
711
+ deprecated: false
712
+ },
713
+ "aria-setsize": {
714
+ type: "integer",
715
+ required: false,
716
+ allowedOn: ["*"],
717
+ deprecated: false
718
+ }
719
+ };
720
+
721
+ // src/core/a11y-checker.ts
722
+ var A11yChecker = class {
723
+ static checkImageAlt(element) {
724
+ const violations = [];
725
+ const images = element.getElementsByTagName("img");
726
+ for (const img of Array.from(images)) {
727
+ if (!img.hasAttribute("alt")) {
728
+ violations.push({
729
+ id: "image-alt",
730
+ description: "Image must have an alt attribute",
731
+ element: img,
732
+ impact: "serious"
733
+ });
734
+ } else if (img.getAttribute("alt")?.trim() === "") {
735
+ violations.push({
736
+ id: "image-alt",
737
+ description: "Image alt attribute must not be empty",
738
+ element: img,
739
+ impact: "serious"
740
+ });
741
+ }
742
+ }
743
+ return violations;
744
+ }
745
+ static checkButtonLabel(element) {
746
+ const violations = [];
747
+ const buttons = element.getElementsByTagName("button");
748
+ for (const button of Array.from(buttons)) {
749
+ if (!button.textContent?.trim() && !button.getAttribute("aria-label")) {
750
+ violations.push({
751
+ id: "button-label",
752
+ description: "Button must have a label or aria-label",
753
+ element: button,
754
+ impact: "critical"
755
+ });
756
+ }
757
+ }
758
+ return violations;
759
+ }
760
+ static checkFormLabels(element) {
761
+ const violations = [];
762
+ const inputs = element.querySelectorAll("input, select, textarea");
763
+ for (const input of Array.from(inputs)) {
764
+ const hasLabel = input.hasAttribute("id") && element.querySelector(`label[for="${input.getAttribute("id")}"]`);
765
+ const hasAriaLabel = input.hasAttribute("aria-label");
766
+ const hasAriaLabelledBy = input.hasAttribute("aria-labelledby") && document.getElementById(input.getAttribute("aria-labelledby") || "");
767
+ if (!hasLabel && !hasAriaLabel && !hasAriaLabelledBy) {
768
+ violations.push({
769
+ id: "form-label",
770
+ description: "Form control must have an associated label",
771
+ element: input,
772
+ impact: "critical"
773
+ });
774
+ }
775
+ }
776
+ return violations;
777
+ }
778
+ static checkHeadingOrder(element) {
779
+ const violations = [];
780
+ const headings = element.querySelectorAll("h1, h2, h3, h4, h5, h6");
781
+ let previousLevel = 0;
782
+ for (const heading of Array.from(headings)) {
783
+ const currentLevel = parseInt(heading.tagName[1]);
784
+ if (previousLevel > 0 && currentLevel - previousLevel > 1) {
785
+ violations.push({
786
+ id: "heading-order",
787
+ description: `Heading level skipped from h${previousLevel} to h${currentLevel}`,
788
+ element: heading,
789
+ impact: "moderate"
790
+ });
791
+ }
792
+ previousLevel = currentLevel;
793
+ }
794
+ return violations;
795
+ }
796
+ static checkLinkText(element) {
797
+ const violations = [];
798
+ const links = element.getElementsByTagName("a");
799
+ for (const link of Array.from(links)) {
800
+ const text = link.textContent?.trim().toLowerCase() || "";
801
+ const ariaLabel = link.getAttribute("aria-label")?.toLowerCase();
802
+ if (!text && !ariaLabel) {
803
+ violations.push({
804
+ id: "link-text",
805
+ description: "Link must have descriptive text",
806
+ element: link,
807
+ impact: "serious"
808
+ });
809
+ } else if (["click here", "read more", "more"].includes(text) && !ariaLabel) {
810
+ violations.push({
811
+ id: "link-text-descriptive",
812
+ description: "Link text should be more descriptive",
813
+ element: link,
814
+ impact: "moderate"
815
+ });
816
+ }
817
+ }
818
+ return violations;
819
+ }
820
+ static checkIframeTitle(element) {
821
+ const violations = [];
822
+ const iframes = element.getElementsByTagName("iframe");
823
+ for (const iframe of Array.from(iframes)) {
824
+ if (!iframe.hasAttribute("title")) {
825
+ violations.push({
826
+ id: "iframe-title",
827
+ description: "iframe must have a title attribute",
828
+ element: iframe,
829
+ impact: "serious"
830
+ });
831
+ } else if (iframe.getAttribute("title")?.trim() === "") {
832
+ violations.push({
833
+ id: "iframe-title",
834
+ description: "iframe title attribute must not be empty",
835
+ element: iframe,
836
+ impact: "serious"
837
+ });
838
+ }
839
+ }
840
+ return violations;
841
+ }
842
+ static checkFieldsetLegend(element) {
843
+ const violations = [];
844
+ const fieldsets = element.getElementsByTagName("fieldset");
845
+ for (const fieldset of Array.from(fieldsets)) {
846
+ const legend = Array.from(fieldset.children).find(
847
+ (child) => child.tagName.toLowerCase() === "legend"
848
+ );
849
+ if (!legend) {
850
+ violations.push({
851
+ id: "fieldset-legend",
852
+ description: "fieldset must have a legend element as a direct child",
853
+ element: fieldset,
854
+ impact: "serious"
855
+ });
856
+ } else if (!legend.textContent?.trim()) {
857
+ violations.push({
858
+ id: "fieldset-legend-empty",
859
+ description: "fieldset legend must have non-empty text content",
860
+ element: fieldset,
861
+ impact: "serious"
862
+ });
863
+ }
864
+ }
865
+ return violations;
866
+ }
867
+ static checkTableStructure(element) {
868
+ const violations = [];
869
+ const tables = element.getElementsByTagName("table");
870
+ for (const table of Array.from(tables)) {
871
+ const hasCaption = table.querySelector("caption");
872
+ const hasAriaLabel = table.hasAttribute("aria-label");
873
+ const hasAriaLabelledBy = table.hasAttribute("aria-labelledby");
874
+ if (!hasCaption && !hasAriaLabel && !hasAriaLabelledBy) {
875
+ violations.push({
876
+ id: "table-caption",
877
+ description: "Table must have a caption or aria-label/aria-labelledby",
878
+ element: table,
879
+ impact: "serious"
880
+ });
881
+ }
882
+ const headerCells = table.querySelectorAll("th");
883
+ const dataCells = table.querySelectorAll("td");
884
+ if (dataCells.length > 0 && headerCells.length === 0) {
885
+ violations.push({
886
+ id: "table-headers",
887
+ description: "Table must have header cells (th elements) when it has data cells",
888
+ element: table,
889
+ impact: "serious"
890
+ });
891
+ }
892
+ for (const th of Array.from(headerCells)) {
893
+ if (!th.hasAttribute("scope")) {
894
+ violations.push({
895
+ id: "table-header-scope",
896
+ description: "Table header cells (th) should have a scope attribute",
897
+ element: th,
898
+ impact: "moderate"
899
+ });
900
+ }
901
+ }
902
+ }
903
+ return violations;
904
+ }
905
+ static checkDetailsSummary(element) {
906
+ const violations = [];
907
+ const detailsElements = element.getElementsByTagName("details");
908
+ for (const details of Array.from(detailsElements)) {
909
+ const firstChild = details.firstElementChild;
910
+ if (!firstChild || firstChild.tagName.toLowerCase() !== "summary") {
911
+ violations.push({
912
+ id: "details-summary",
913
+ description: "details element must have a summary element as its first child",
914
+ element: details,
915
+ impact: "serious"
916
+ });
917
+ } else if (!firstChild.textContent?.trim()) {
918
+ violations.push({
919
+ id: "details-summary-empty",
920
+ description: "details summary element must have non-empty text content",
921
+ element: details,
922
+ impact: "serious"
923
+ });
924
+ }
925
+ }
926
+ return violations;
927
+ }
928
+ static checkVideoCaptions(element) {
929
+ const violations = [];
930
+ const videos = element.getElementsByTagName("video");
931
+ for (const video of Array.from(videos)) {
932
+ const tracks = video.querySelectorAll("track");
933
+ const captionTracks = Array.from(tracks).filter(
934
+ (track) => track.getAttribute("kind")?.toLowerCase() === "captions"
935
+ );
936
+ if (captionTracks.length === 0) {
937
+ violations.push({
938
+ id: "video-captions",
939
+ description: 'Video element must have at least one track element with kind="captions"',
940
+ element: video,
941
+ impact: "serious"
942
+ });
943
+ } else {
944
+ for (const track of captionTracks) {
945
+ if (!track.hasAttribute("srclang")) {
946
+ violations.push({
947
+ id: "video-track-srclang",
948
+ description: "Video caption track must have a srclang attribute",
949
+ element: track,
950
+ impact: "serious"
951
+ });
952
+ }
953
+ if (!track.hasAttribute("label")) {
954
+ violations.push({
955
+ id: "video-track-label",
956
+ description: "Video caption track should have a label attribute",
957
+ element: track,
958
+ impact: "moderate"
959
+ });
960
+ }
961
+ }
962
+ }
963
+ }
964
+ return violations;
965
+ }
966
+ static checkAudioCaptions(element) {
967
+ const violations = [];
968
+ const audios = element.getElementsByTagName("audio");
969
+ for (const audio of Array.from(audios)) {
970
+ const tracks = audio.querySelectorAll("track");
971
+ const hasTranscript = audio.hasAttribute("aria-describedby") || audio.querySelector('a[href*="transcript"]') || audio.closest("div")?.querySelector('a[href*="transcript"]');
972
+ if (tracks.length === 0 && !hasTranscript) {
973
+ violations.push({
974
+ id: "audio-captions",
975
+ description: "Audio element must have track elements or a transcript link",
976
+ element: audio,
977
+ impact: "serious"
978
+ });
979
+ } else if (tracks.length > 0) {
980
+ for (const track of Array.from(tracks)) {
981
+ if (!track.hasAttribute("srclang")) {
982
+ violations.push({
983
+ id: "audio-track-srclang",
984
+ description: "Audio track must have a srclang attribute",
985
+ element: track,
986
+ impact: "serious"
987
+ });
988
+ }
989
+ if (!track.hasAttribute("label")) {
990
+ violations.push({
991
+ id: "audio-track-label",
992
+ description: "Audio track should have a label attribute",
993
+ element: track,
994
+ impact: "moderate"
995
+ });
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ return violations;
1001
+ }
1002
+ static checkLandmarks(element) {
1003
+ const violations = [];
1004
+ const landmarkTags = ["nav", "main", "header", "footer", "aside", "section", "article"];
1005
+ const mainElements = element.getElementsByTagName("main");
1006
+ if (mainElements.length > 1) {
1007
+ const mains = Array.from(mainElements);
1008
+ for (let i = 1; i < mains.length; i++) {
1009
+ violations.push({
1010
+ id: "landmark-multiple-main",
1011
+ description: "Page should have only one main element",
1012
+ element: mains[i],
1013
+ impact: "serious"
1014
+ });
1015
+ }
1016
+ }
1017
+ for (const tag of landmarkTags) {
1018
+ const landmarks = element.getElementsByTagName(tag);
1019
+ for (const landmark of Array.from(landmarks)) {
1020
+ if (tag === "section" || tag === "article") {
1021
+ const hasHeading = landmark.querySelector("h1, h2, h3, h4, h5, h6");
1022
+ const hasAriaLabel = landmark.hasAttribute("aria-label");
1023
+ const hasAriaLabelledBy = landmark.hasAttribute("aria-labelledby");
1024
+ if (!hasHeading && !hasAriaLabel && !hasAriaLabelledBy) {
1025
+ violations.push({
1026
+ id: "landmark-missing-name",
1027
+ description: `${tag} element should have an accessible name (heading, aria-label, or aria-labelledby)`,
1028
+ element: landmark,
1029
+ impact: "moderate"
1030
+ });
1031
+ }
1032
+ }
1033
+ if (tag === "nav" || tag === "aside") {
1034
+ const hasAriaLabel = landmark.hasAttribute("aria-label");
1035
+ const hasAriaLabelledBy = landmark.hasAttribute("aria-labelledby");
1036
+ const sameType = Array.from(element.getElementsByTagName(tag));
1037
+ const unnamed = sameType.filter(
1038
+ (l) => !l.hasAttribute("aria-label") && !l.hasAttribute("aria-labelledby")
1039
+ );
1040
+ if (unnamed.length > 1 && !hasAriaLabel && !hasAriaLabelledBy) {
1041
+ violations.push({
1042
+ id: "landmark-duplicate-unnamed",
1043
+ description: `Multiple ${tag} elements found. Each should have an accessible name (aria-label or aria-labelledby)`,
1044
+ element: landmark,
1045
+ impact: "moderate"
1046
+ });
1047
+ }
1048
+ }
1049
+ }
1050
+ }
1051
+ return violations;
1052
+ }
1053
+ static checkDialogModal(element) {
1054
+ const violations = [];
1055
+ const dialogs = element.getElementsByTagName("dialog");
1056
+ for (const dialog of Array.from(dialogs)) {
1057
+ const hasAriaLabel = dialog.hasAttribute("aria-label");
1058
+ const hasAriaLabelledBy = dialog.hasAttribute("aria-labelledby");
1059
+ const hasHeading = dialog.querySelector("h1, h2, h3, h4, h5, h6");
1060
+ if (!hasAriaLabel && !hasAriaLabelledBy && !hasHeading) {
1061
+ violations.push({
1062
+ id: "dialog-missing-name",
1063
+ description: "Dialog element must have an accessible name (aria-label, aria-labelledby, or heading)",
1064
+ element: dialog,
1065
+ impact: "serious"
1066
+ });
1067
+ }
1068
+ const isModal = dialog.hasAttribute("open") || dialog.getAttribute("aria-modal") === "true";
1069
+ if (isModal && !dialog.hasAttribute("aria-modal")) {
1070
+ violations.push({
1071
+ id: "dialog-missing-modal",
1072
+ description: 'Modal dialog should have aria-modal="true" attribute',
1073
+ element: dialog,
1074
+ impact: "moderate"
1075
+ });
1076
+ }
1077
+ const hasRole = dialog.hasAttribute("role");
1078
+ const roleValue = dialog.getAttribute("role");
1079
+ if (hasRole && roleValue !== "dialog" && roleValue !== "alertdialog") {
1080
+ violations.push({
1081
+ id: "dialog-invalid-role",
1082
+ description: 'Dialog element should have role="dialog" or role="alertdialog"',
1083
+ element: dialog,
1084
+ impact: "moderate"
1085
+ });
1086
+ }
1087
+ }
1088
+ return violations;
1089
+ }
1090
+ static async check(element) {
1091
+ const violations = [
1092
+ ...this.checkImageAlt(element),
1093
+ ...this.checkLinkText(element),
1094
+ ...this.checkButtonLabel(element),
1095
+ ...this.checkFormLabels(element),
1096
+ ...this.checkHeadingOrder(element),
1097
+ ...this.checkIframeTitle(element),
1098
+ ...this.checkFieldsetLegend(element),
1099
+ ...this.checkTableStructure(element),
1100
+ ...this.checkDetailsSummary(element),
1101
+ ...this.checkVideoCaptions(element),
1102
+ ...this.checkAudioCaptions(element),
1103
+ ...this.checkLandmarks(element),
1104
+ ...this.checkDialogModal(element),
1105
+ // Phase 1: ARIA Validation
1106
+ ...this.checkAriaRoles(element),
1107
+ ...this.checkAriaProperties(element),
1108
+ ...this.checkAriaRelationships(element),
1109
+ ...this.checkAccessibleName(element),
1110
+ ...this.checkCompositePatterns(element),
1111
+ // Phase 1: Semantic HTML Validation
1112
+ ...this.checkSemanticHTML(element),
1113
+ // Phase 1: Form Validation Messages
1114
+ ...this.checkFormValidationMessages(element)
1115
+ ];
1116
+ if (violations.length > 0) {
1117
+ console.warn("\nAccessibility Violations Found:");
1118
+ violations.forEach((violation, index) => {
1119
+ console.warn(`
1120
+ ${index + 1}. ${violation.id} (${violation.impact})`);
1121
+ console.warn(` Description: ${violation.description}`);
1122
+ console.warn(` Element: ${violation.element.outerHTML}`);
1123
+ });
1124
+ console.warn("\n");
1125
+ }
1126
+ return { violations };
1127
+ }
1128
+ /**
1129
+ * Check ARIA roles for validity, appropriateness, and context
1130
+ */
1131
+ static checkAriaRoles(element) {
1132
+ const violations = [];
1133
+ const allElements = element.querySelectorAll("[role]");
1134
+ for (const el of Array.from(allElements)) {
1135
+ const role = el.getAttribute("role");
1136
+ if (!role)
1137
+ continue;
1138
+ const tagName = el.tagName.toLowerCase();
1139
+ const inputType = el.getAttribute("type");
1140
+ const elementKey = inputType ? `${tagName}[type="${inputType}"]` : tagName;
1141
+ if (!ARIA_ROLES[role]) {
1142
+ violations.push({
1143
+ id: "aria-invalid-role",
1144
+ description: `Invalid ARIA role: ${role}`,
1145
+ element: el,
1146
+ impact: "serious"
1147
+ });
1148
+ continue;
1149
+ }
1150
+ const roleDef = ARIA_ROLES[role];
1151
+ if (DEPRECATED_ARIA.roles.includes(role)) {
1152
+ violations.push({
1153
+ id: "aria-deprecated-role",
1154
+ description: `ARIA role "${role}" is deprecated`,
1155
+ element: el,
1156
+ impact: "moderate"
1157
+ });
1158
+ }
1159
+ if (roleDef.abstract) {
1160
+ violations.push({
1161
+ id: "aria-abstract-role",
1162
+ description: `ARIA role "${role}" is abstract and should not be used`,
1163
+ element: el,
1164
+ impact: "moderate"
1165
+ });
1166
+ }
1167
+ const implicitRole = ARIA_IN_HTML.implicitRoles[elementKey] || ARIA_IN_HTML.implicitRoles[tagName] || null;
1168
+ if (implicitRole === role) {
1169
+ violations.push({
1170
+ id: "aria-redundant-role",
1171
+ description: `Redundant role: <${tagName}> already has implicit role "${role}"`,
1172
+ element: el,
1173
+ impact: "minor"
1174
+ });
1175
+ }
1176
+ if (implicitRole && implicitRole !== role && this.hasStrongNativeSemantics(tagName)) {
1177
+ violations.push({
1178
+ id: "aria-conflicting-semantics",
1179
+ description: `Conflicting semantics: <${tagName}> has implicit role "${implicitRole}" but role="${role}" is specified`,
1180
+ element: el,
1181
+ impact: "serious"
1182
+ });
1183
+ }
1184
+ if (!roleDef.allowedOn.includes("*") && !roleDef.allowedOn.includes(tagName)) {
1185
+ violations.push({
1186
+ id: "aria-role-on-wrong-element",
1187
+ description: `Role "${role}" is not appropriate for <${tagName}>`,
1188
+ element: el,
1189
+ impact: "serious"
1190
+ });
1191
+ }
1192
+ if (roleDef.requiredContext) {
1193
+ const parent = el.parentElement;
1194
+ const parentRole = parent?.getAttribute("role");
1195
+ const contexts = Array.isArray(roleDef.requiredContext) ? roleDef.requiredContext : [roleDef.requiredContext];
1196
+ if (!parentRole || !contexts.includes(parentRole)) {
1197
+ violations.push({
1198
+ id: "aria-missing-context-role",
1199
+ description: `Role "${role}" must be in context of ${contexts.join(" or ")}`,
1200
+ element: el,
1201
+ impact: "serious"
1202
+ });
1203
+ }
1204
+ }
1205
+ if (roleDef.requiredProperties.length > 0) {
1206
+ const hasRequired = roleDef.requiredProperties.some((prop) => el.hasAttribute(prop));
1207
+ if (!hasRequired) {
1208
+ violations.push({
1209
+ id: "aria-missing-required-property",
1210
+ description: `Role "${role}" requires one of: ${roleDef.requiredProperties.join(", ")}`,
1211
+ element: el,
1212
+ impact: "critical"
1213
+ });
1214
+ }
1215
+ }
1216
+ }
1217
+ return violations;
1218
+ }
1219
+ static hasStrongNativeSemantics(tagName) {
1220
+ const strongSemanticElements = ["button", "a", "input", "select", "textarea", "img", "h1", "h2", "h3", "h4", "h5", "h6"];
1221
+ return strongSemanticElements.includes(tagName);
1222
+ }
1223
+ /**
1224
+ * Check ARIA properties for validity, appropriateness, and values
1225
+ */
1226
+ static checkAriaProperties(element) {
1227
+ const violations = [];
1228
+ const allElements = Array.from(element.querySelectorAll("*")).filter((el) => {
1229
+ return Array.from(el.attributes).some((attr) => attr.name.startsWith("aria-"));
1230
+ });
1231
+ for (const el of Array.from(allElements)) {
1232
+ const attributes = Array.from(el.attributes).filter((attr) => attr.name.startsWith("aria-"));
1233
+ for (const attr of attributes) {
1234
+ const propName = attr.name;
1235
+ const propValue = attr.value;
1236
+ const tagName = el.tagName.toLowerCase();
1237
+ const inputType = el.getAttribute("type");
1238
+ const elementKey = inputType ? `${tagName}[type="${inputType}"]` : tagName;
1239
+ if (!ARIA_PROPERTIES[propName]) {
1240
+ violations.push({
1241
+ id: "aria-invalid-property",
1242
+ description: `Invalid ARIA property: ${propName}`,
1243
+ element: el,
1244
+ impact: "serious"
1245
+ });
1246
+ continue;
1247
+ }
1248
+ const propDef = ARIA_PROPERTIES[propName];
1249
+ if (propDef.deprecated || DEPRECATED_ARIA.properties.includes(propName)) {
1250
+ violations.push({
1251
+ id: "aria-deprecated-property",
1252
+ description: `ARIA property "${propName}" is deprecated`,
1253
+ element: el,
1254
+ impact: "moderate"
1255
+ });
1256
+ }
1257
+ const discouraged = ARIA_IN_HTML.discouraged[elementKey] || ARIA_IN_HTML.discouraged[tagName];
1258
+ if (discouraged && discouraged.includes(propName)) {
1259
+ violations.push({
1260
+ id: "aria-property-discouraged",
1261
+ description: `${propName} is discouraged on <${tagName}> (use native HTML instead)`,
1262
+ element: el,
1263
+ impact: "moderate"
1264
+ });
1265
+ }
1266
+ if (!this.validateAriaPropertyValue(propValue, propDef.type, propDef.enumValues)) {
1267
+ violations.push({
1268
+ id: "aria-invalid-property-value",
1269
+ description: `Invalid value for ${propName}: ${propValue}. Expected ${propDef.type}${propDef.enumValues ? ` (${propDef.enumValues.join(", ")})` : ""}`,
1270
+ element: el,
1271
+ impact: "serious"
1272
+ });
1273
+ }
1274
+ if (propName === "aria-label" && propValue.trim() === "") {
1275
+ violations.push({
1276
+ id: "aria-label-empty",
1277
+ description: "aria-label must not be empty",
1278
+ element: el,
1279
+ impact: "serious"
1280
+ });
1281
+ }
1282
+ if (!propDef.allowedOn.includes("*") && !propDef.allowedOn.includes(tagName)) {
1283
+ violations.push({
1284
+ id: "aria-property-on-wrong-element",
1285
+ description: `${propName} is not appropriate for <${tagName}>`,
1286
+ element: el,
1287
+ impact: "moderate"
1288
+ });
1289
+ }
1290
+ }
1291
+ }
1292
+ return violations;
1293
+ }
1294
+ /**
1295
+ * Validate ARIA property value based on type
1296
+ */
1297
+ static validateAriaPropertyValue(value, type, enumValues) {
1298
+ switch (type) {
1299
+ case "boolean": {
1300
+ return value === "true" || value === "false";
1301
+ }
1302
+ case "tristate": {
1303
+ return value === "true" || value === "false" || value === "mixed";
1304
+ }
1305
+ case "idref": {
1306
+ return value.length > 0 && /^[a-zA-Z][\w-]*$/.test(value);
1307
+ }
1308
+ case "idrefs": {
1309
+ if (value.trim() === "")
1310
+ return false;
1311
+ const ids = value.trim().split(/\s+/);
1312
+ return ids.every((id) => /^[a-zA-Z][\w-]*$/.test(id));
1313
+ }
1314
+ case "string": {
1315
+ return true;
1316
+ }
1317
+ case "enum": {
1318
+ if (!enumValues)
1319
+ return true;
1320
+ return enumValues.includes(value);
1321
+ }
1322
+ case "integer": {
1323
+ const intValue = parseInt(value, 10);
1324
+ return !isNaN(intValue) && intValue.toString() === value;
1325
+ }
1326
+ case "number": {
1327
+ const numValue = parseFloat(value);
1328
+ return !isNaN(numValue) && isFinite(numValue);
1329
+ }
1330
+ default: {
1331
+ return true;
1332
+ }
1333
+ }
1334
+ }
1335
+ /**
1336
+ * Check ARIA relationships (ID references) with enhanced validation
1337
+ */
1338
+ static checkAriaRelationships(element) {
1339
+ const violations = [];
1340
+ const checkIdReferences = (elements, attribute, violationId) => {
1341
+ for (const el of Array.from(elements)) {
1342
+ const ids = el.getAttribute(attribute)?.split(/\s+/) || [];
1343
+ const elementId = el.id || el.getAttribute("id");
1344
+ if (ids.includes(elementId || "self")) {
1345
+ violations.push({
1346
+ id: `${violationId}-self-reference`,
1347
+ description: `${attribute} should not reference itself`,
1348
+ element: el,
1349
+ impact: "serious"
1350
+ });
1351
+ }
1352
+ for (const id of ids) {
1353
+ if (!id)
1354
+ continue;
1355
+ const referencedEl = element.querySelector(`#${id}`);
1356
+ if (!referencedEl) {
1357
+ violations.push({
1358
+ id: `${violationId}-reference-missing`,
1359
+ description: `${attribute} references non-existent ID: ${id}`,
1360
+ element: el,
1361
+ impact: "serious"
1362
+ });
1363
+ continue;
1364
+ }
1365
+ if (referencedEl.getAttribute("aria-hidden") === "true") {
1366
+ violations.push({
1367
+ id: `${violationId}-reference-hidden`,
1368
+ description: `${attribute} references element with aria-hidden="true" (name/description will be hidden from AT)`,
1369
+ element: el,
1370
+ impact: "serious"
1371
+ });
1372
+ }
1373
+ const allWithId = element.querySelectorAll(`#${id}`);
1374
+ if (allWithId.length > 1) {
1375
+ violations.push({
1376
+ id: `${violationId}-duplicate-id`,
1377
+ description: `ID "${id}" is not unique in document (referenced by ${attribute})`,
1378
+ element: el,
1379
+ impact: "serious"
1380
+ });
1381
+ }
1382
+ }
1383
+ }
1384
+ };
1385
+ checkIdReferences(
1386
+ element.querySelectorAll("[aria-labelledby]"),
1387
+ "aria-labelledby",
1388
+ "aria-labelledby"
1389
+ );
1390
+ checkIdReferences(
1391
+ element.querySelectorAll("[aria-describedby]"),
1392
+ "aria-describedby",
1393
+ "aria-describedby"
1394
+ );
1395
+ checkIdReferences(
1396
+ element.querySelectorAll("[aria-controls]"),
1397
+ "aria-controls",
1398
+ "aria-controls"
1399
+ );
1400
+ checkIdReferences(
1401
+ element.querySelectorAll("[aria-owns]"),
1402
+ "aria-owns",
1403
+ "aria-owns"
1404
+ );
1405
+ const ownsElements = element.querySelectorAll("[aria-owns]");
1406
+ for (const el of Array.from(ownsElements)) {
1407
+ const ids = el.getAttribute("aria-owns")?.split(/\s+/) || [];
1408
+ const elementId = el.id || el.getAttribute("id");
1409
+ for (const id of ids) {
1410
+ if (!id)
1411
+ continue;
1412
+ const referencedEl = element.querySelector(`#${id}`);
1413
+ if (referencedEl && referencedEl.getAttribute("aria-owns")?.includes(elementId || "")) {
1414
+ violations.push({
1415
+ id: "aria-owns-circular-reference",
1416
+ description: "Circular aria-owns reference detected",
1417
+ element: el,
1418
+ impact: "serious"
1419
+ });
1420
+ }
1421
+ }
1422
+ }
1423
+ const activedescendantElements = element.querySelectorAll("[aria-activedescendant]");
1424
+ for (const el of Array.from(activedescendantElements)) {
1425
+ const id = el.getAttribute("aria-activedescendant");
1426
+ if (!id)
1427
+ continue;
1428
+ const referencedEl = element.querySelector(`#${id}`);
1429
+ if (!referencedEl) {
1430
+ violations.push({
1431
+ id: "aria-activedescendant-reference-missing",
1432
+ description: `aria-activedescendant references non-existent ID: ${id}`,
1433
+ element: el,
1434
+ impact: "serious"
1435
+ });
1436
+ continue;
1437
+ }
1438
+ const isFocusable = this.isFocusable(el);
1439
+ if (!isFocusable) {
1440
+ violations.push({
1441
+ id: "aria-activedescendant-owner-not-focusable",
1442
+ description: "Element with aria-activedescendant should be focusable",
1443
+ element: el,
1444
+ impact: "serious"
1445
+ });
1446
+ }
1447
+ }
1448
+ return violations;
1449
+ }
1450
+ /**
1451
+ * Check if element is focusable
1452
+ */
1453
+ static isFocusable(element) {
1454
+ const tagName = element.tagName.toLowerCase();
1455
+ const tabindex = element.getAttribute("tabindex");
1456
+ if (["a", "button", "input", "select", "textarea"].includes(tagName)) {
1457
+ return !element.hasAttribute("disabled");
1458
+ }
1459
+ if (tabindex !== null) {
1460
+ return tabindex !== "-1";
1461
+ }
1462
+ return false;
1463
+ }
1464
+ /**
1465
+ * Check accessible name computation
1466
+ */
1467
+ static checkAccessibleName(element) {
1468
+ const violations = [];
1469
+ const elementsRequiringName = element.querySelectorAll(
1470
+ 'button, a, input, select, textarea, dialog, [role="button"], [role="link"], [role="dialog"], [role="alertdialog"]'
1471
+ );
1472
+ for (const el of Array.from(elementsRequiringName)) {
1473
+ const ariaLabel = el.getAttribute("aria-label");
1474
+ const ariaLabelledBy = el.getAttribute("aria-labelledby");
1475
+ const textContent = el.textContent?.trim() || "";
1476
+ const tagName = el.tagName.toLowerCase();
1477
+ if (ariaLabel === "") {
1478
+ violations.push({
1479
+ id: "aria-label-empty",
1480
+ description: "aria-label must not be empty",
1481
+ element: el,
1482
+ impact: "serious"
1483
+ });
1484
+ }
1485
+ if (ariaLabelledBy) {
1486
+ const ids = ariaLabelledBy.split(/\s+/);
1487
+ for (const id of ids) {
1488
+ if (!id)
1489
+ continue;
1490
+ const labelEl = element.querySelector(`#${id}`);
1491
+ if (labelEl) {
1492
+ const labelText = labelEl.textContent?.trim() || "";
1493
+ const labelAriaLabel = labelEl.getAttribute("aria-label");
1494
+ if (!labelText && !labelAriaLabel) {
1495
+ violations.push({
1496
+ id: "aria-labelledby-empty-reference",
1497
+ description: `aria-labelledby references element with no accessible text: ${id}`,
1498
+ element: el,
1499
+ impact: "serious"
1500
+ });
1501
+ }
1502
+ }
1503
+ }
1504
+ }
1505
+ if (textContent && ariaLabel && textContent !== ariaLabel) {
1506
+ if (tagName === "button" && textContent.length > 0 && ariaLabel.length > 0) {
1507
+ violations.push({
1508
+ id: "aria-label-content-mismatch",
1509
+ description: `Button has visible text "${textContent}" but aria-label is "${ariaLabel}" - ensure they match or use aria-label only when text is not descriptive`,
1510
+ element: el,
1511
+ impact: "moderate"
1512
+ });
1513
+ }
1514
+ }
1515
+ if (ariaLabel && tagName === "input" && el.getAttribute("type") !== "hidden") {
1516
+ const hasLabel = element.querySelector(`label[for="${el.id}"]`) || el.closest("label");
1517
+ if (hasLabel) {
1518
+ violations.push({
1519
+ id: "aria-label-with-visible-label",
1520
+ description: "aria-label should only be used when visible label is not available. Consider using <label> instead.",
1521
+ element: el,
1522
+ impact: "moderate"
1523
+ });
1524
+ }
1525
+ }
1526
+ const role = el.getAttribute("role");
1527
+ if (role === "dialog" || role === "alertdialog" || tagName === "dialog") {
1528
+ const hasHeading = el.querySelector("h1, h2, h3, h4, h5, h6");
1529
+ const hasAriaLabel = ariaLabel && ariaLabel.trim() !== "";
1530
+ const hasAriaLabelledBy = ariaLabelledBy && ariaLabelledBy.trim() !== "";
1531
+ if (hasHeading && !hasAriaLabelledBy) {
1532
+ violations.push({
1533
+ id: "dialog-prefer-labelledby",
1534
+ description: "Dialog with visible heading should use aria-labelledby instead of aria-label",
1535
+ element: el,
1536
+ impact: "moderate"
1537
+ });
1538
+ }
1539
+ if (!hasAriaLabel && !hasAriaLabelledBy && !hasHeading) {
1540
+ violations.push({
1541
+ id: "dialog-missing-name",
1542
+ description: "Dialog must have accessible name (aria-label, aria-labelledby, or heading)",
1543
+ element: el,
1544
+ impact: "critical"
1545
+ });
1546
+ }
1547
+ }
1548
+ }
1549
+ return violations;
1550
+ }
1551
+ /**
1552
+ * Check composite patterns (tab/listbox/menu/tree)
1553
+ */
1554
+ static checkCompositePatterns(element) {
1555
+ const violations = [];
1556
+ const tabs = element.querySelectorAll('[role="tab"]');
1557
+ for (const tab of Array.from(tabs)) {
1558
+ const tablist = tab.closest('[role="tablist"]');
1559
+ if (!tablist) {
1560
+ violations.push({
1561
+ id: "tab-missing-tablist",
1562
+ description: "Tab must be inside a tablist",
1563
+ element: tab,
1564
+ impact: "serious"
1565
+ });
1566
+ }
1567
+ const controls = tab.getAttribute("aria-controls");
1568
+ if (controls) {
1569
+ const tabpanel = element.querySelector(`#${controls}`);
1570
+ if (!tabpanel || tabpanel.getAttribute("role") !== "tabpanel") {
1571
+ violations.push({
1572
+ id: "tab-invalid-controls",
1573
+ description: "Tab aria-controls must reference a tabpanel",
1574
+ element: tab,
1575
+ impact: "serious"
1576
+ });
1577
+ }
1578
+ }
1579
+ }
1580
+ const listboxes = element.querySelectorAll('[role="listbox"]');
1581
+ for (const listbox of Array.from(listboxes)) {
1582
+ const options = listbox.querySelectorAll('[role="option"]');
1583
+ if (options.length === 0) {
1584
+ violations.push({
1585
+ id: "listbox-missing-options",
1586
+ description: "Listbox must contain option elements",
1587
+ element: listbox,
1588
+ impact: "serious"
1589
+ });
1590
+ }
1591
+ }
1592
+ const menus = element.querySelectorAll('[role="menu"], [role="menubar"]');
1593
+ for (const menu of Array.from(menus)) {
1594
+ const menuitems = menu.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]');
1595
+ if (menuitems.length === 0) {
1596
+ violations.push({
1597
+ id: "menu-missing-menuitems",
1598
+ description: "Menu must contain menuitem elements",
1599
+ element: menu,
1600
+ impact: "serious"
1601
+ });
1602
+ }
1603
+ }
1604
+ const trees = element.querySelectorAll('[role="tree"]');
1605
+ for (const tree of Array.from(trees)) {
1606
+ const treeitems = tree.querySelectorAll('[role="treeitem"]');
1607
+ if (treeitems.length === 0) {
1608
+ violations.push({
1609
+ id: "tree-missing-treeitems",
1610
+ description: "Tree must contain treeitem elements",
1611
+ element: tree,
1612
+ impact: "serious"
1613
+ });
1614
+ }
1615
+ }
1616
+ const interactiveRoles = ["button", "link", "menuitem", "tab", "option"];
1617
+ for (const role of interactiveRoles) {
1618
+ const elements = element.querySelectorAll(`[role="${role}"]`);
1619
+ for (const el of Array.from(elements)) {
1620
+ const tagName = el.tagName.toLowerCase();
1621
+ if (!["button", "a", "input"].includes(tagName)) {
1622
+ const tabindex = el.getAttribute("tabindex");
1623
+ if (tabindex === null || tabindex === "-1") {
1624
+ violations.push({
1625
+ id: "interactive-role-not-focusable",
1626
+ description: `Element with role="${role}" should be focusable (add tabindex="0" or use native element)`,
1627
+ element: el,
1628
+ impact: "serious"
1629
+ });
1630
+ }
1631
+ }
1632
+ }
1633
+ }
1634
+ return violations;
1635
+ }
1636
+ /**
1637
+ * Check semantic HTML usage and structure
1638
+ */
1639
+ static checkSemanticHTML(element) {
1640
+ const violations = [];
1641
+ const divsWithRole = element.querySelectorAll("div[role], span[role]");
1642
+ for (const el of Array.from(divsWithRole)) {
1643
+ const role = el.getAttribute("role");
1644
+ const tagName = el.tagName.toLowerCase();
1645
+ const roleToElement = {
1646
+ "button": "button",
1647
+ "link": "a",
1648
+ "heading": "h1-h6",
1649
+ "list": "ul or ol",
1650
+ "listitem": "li",
1651
+ "navigation": "nav",
1652
+ "main": "main",
1653
+ "article": "article",
1654
+ "section": "section",
1655
+ "banner": "header",
1656
+ "contentinfo": "footer",
1657
+ "complementary": "aside",
1658
+ "form": "form",
1659
+ "dialog": "dialog"
1660
+ };
1661
+ if (role && roleToElement[role]) {
1662
+ violations.push({
1663
+ id: "semantic-element-preferred",
1664
+ description: `Use <${roleToElement[role]}> instead of <${tagName} role="${role}">`,
1665
+ element: el,
1666
+ impact: "moderate"
1667
+ });
1668
+ }
1669
+ }
1670
+ const interactiveSelectors = 'button, a[href], input[type="button"], input[type="submit"], input[type="reset"], [role="button"], [role="link"]';
1671
+ const interactiveElements = element.querySelectorAll(interactiveSelectors);
1672
+ for (const el of Array.from(interactiveElements)) {
1673
+ const parent = el.parentElement;
1674
+ if (parent && parent.matches(interactiveSelectors)) {
1675
+ violations.push({
1676
+ id: "nested-interactive",
1677
+ description: "Interactive element cannot be nested inside another interactive element",
1678
+ element: el,
1679
+ impact: "serious"
1680
+ });
1681
+ }
1682
+ }
1683
+ const anchors = element.querySelectorAll("a");
1684
+ for (const anchor of Array.from(anchors)) {
1685
+ if (!anchor.hasAttribute("href")) {
1686
+ violations.push({
1687
+ id: "anchor-without-href",
1688
+ description: "<a> without href should be <button> or have href attribute",
1689
+ element: anchor,
1690
+ impact: "moderate"
1691
+ });
1692
+ }
1693
+ }
1694
+ const forms = element.querySelectorAll("form");
1695
+ for (const form of Array.from(forms)) {
1696
+ const buttons = form.querySelectorAll("button");
1697
+ for (const button of Array.from(buttons)) {
1698
+ if (!button.hasAttribute("type")) {
1699
+ violations.push({
1700
+ id: "button-missing-type",
1701
+ description: '<button> in form should have type="button" to prevent accidental submit',
1702
+ element: button,
1703
+ impact: "moderate"
1704
+ });
1705
+ }
1706
+ }
1707
+ }
1708
+ const listItems = element.querySelectorAll("li");
1709
+ for (const li of Array.from(listItems)) {
1710
+ const parent = li.parentElement;
1711
+ const parentTag = parent?.tagName.toLowerCase();
1712
+ if (parent && parentTag !== "ul" && parentTag !== "ol" && parentTag !== "menu") {
1713
+ violations.push({
1714
+ id: "list-item-outside-list",
1715
+ description: "<li> must be a child of <ul>, <ol>, or <menu>",
1716
+ element: li,
1717
+ impact: "serious"
1718
+ });
1719
+ }
1720
+ }
1721
+ const dts = element.querySelectorAll("dt, dd");
1722
+ for (const el of Array.from(dts)) {
1723
+ const dl = el.closest("dl");
1724
+ if (!dl) {
1725
+ violations.push({
1726
+ id: "dt-dd-outside-dl",
1727
+ description: `<${el.tagName.toLowerCase()}> must appear under a <dl>`,
1728
+ element: el,
1729
+ impact: "serious"
1730
+ });
1731
+ }
1732
+ }
1733
+ const figcaptions = element.querySelectorAll("figcaption");
1734
+ for (const figcaption of Array.from(figcaptions)) {
1735
+ const figure = figcaption.closest("figure");
1736
+ if (!figure) {
1737
+ violations.push({
1738
+ id: "figcaption-outside-figure",
1739
+ description: "<figcaption> must be inside <figure>",
1740
+ element: figcaption,
1741
+ impact: "serious"
1742
+ });
1743
+ }
1744
+ }
1745
+ const figures = element.querySelectorAll("figure");
1746
+ for (const figure of Array.from(figures)) {
1747
+ const figcaptions2 = figure.querySelectorAll("figcaption");
1748
+ if (figcaptions2.length > 1) {
1749
+ violations.push({
1750
+ id: "figure-multiple-figcaptions",
1751
+ description: "<figure> should not have multiple <figcaption> elements",
1752
+ element: figure,
1753
+ impact: "serious"
1754
+ });
1755
+ }
1756
+ }
1757
+ const mains = element.querySelectorAll("main");
1758
+ if (mains.length > 1) {
1759
+ for (const main of Array.from(mains)) {
1760
+ violations.push({
1761
+ id: "multiple-main",
1762
+ description: "Document should have only one <main> element",
1763
+ element: main,
1764
+ impact: "serious"
1765
+ });
1766
+ }
1767
+ }
1768
+ const navs = element.querySelectorAll("nav");
1769
+ if (navs.length > 1) {
1770
+ for (const nav of Array.from(navs)) {
1771
+ const hasLabel = nav.hasAttribute("aria-label") || nav.hasAttribute("aria-labelledby");
1772
+ if (!hasLabel) {
1773
+ violations.push({
1774
+ id: "multiple-nav-without-label",
1775
+ description: "Multiple <nav> elements require accessible names (aria-label or aria-labelledby)",
1776
+ element: nav,
1777
+ impact: "moderate"
1778
+ });
1779
+ }
1780
+ }
1781
+ }
1782
+ const formControls = element.querySelectorAll("input, select, textarea");
1783
+ for (const control of Array.from(formControls)) {
1784
+ if (control.getAttribute("type") === "hidden")
1785
+ continue;
1786
+ const id = control.id;
1787
+ const hasAriaLabel = control.hasAttribute("aria-label");
1788
+ const hasAriaLabelledBy = control.hasAttribute("aria-labelledby");
1789
+ const isWrappedInLabel = control.closest("label");
1790
+ const hasLabelFor = id && element.querySelector(`label[for="${id}"]`);
1791
+ if (!hasAriaLabel && !hasAriaLabelledBy && !isWrappedInLabel && !hasLabelFor) {
1792
+ violations.push({
1793
+ id: "form-control-missing-label",
1794
+ description: "Form control must have associated label",
1795
+ element: control,
1796
+ impact: "serious"
1797
+ });
1798
+ }
1799
+ }
1800
+ const ids = /* @__PURE__ */ new Map();
1801
+ const elementsWithIds = element.querySelectorAll("[id]");
1802
+ for (const el of Array.from(elementsWithIds)) {
1803
+ const id = el.id;
1804
+ if (id) {
1805
+ if (!ids.has(id)) {
1806
+ ids.set(id, []);
1807
+ }
1808
+ const elements = ids.get(id);
1809
+ if (elements) {
1810
+ elements.push(el);
1811
+ }
1812
+ }
1813
+ }
1814
+ for (const [id, elements] of ids.entries()) {
1815
+ if (elements.length > 1) {
1816
+ for (const el of elements) {
1817
+ violations.push({
1818
+ id: "duplicate-id",
1819
+ description: `Duplicate ID "${id}" found - IDs must be unique`,
1820
+ element: el,
1821
+ impact: "serious"
1822
+ });
1823
+ }
1824
+ }
1825
+ }
1826
+ return violations;
1827
+ }
1828
+ /**
1829
+ * Check form validation messages
1830
+ */
1831
+ static checkFormValidationMessages(element) {
1832
+ const violations = [];
1833
+ const invalidElements = element.querySelectorAll("[aria-invalid]");
1834
+ for (const el of Array.from(invalidElements)) {
1835
+ const ariaInvalid = el.getAttribute("aria-invalid");
1836
+ if (ariaInvalid === "true") {
1837
+ const describedBy = el.getAttribute("aria-describedby");
1838
+ if (describedBy) {
1839
+ const ids = describedBy.split(/\s+/);
1840
+ let hasErrorMessage = false;
1841
+ for (const id of ids) {
1842
+ const errorEl = element.querySelector(`#${id}`);
1843
+ if (errorEl) {
1844
+ const errorText = errorEl.textContent?.trim() || "";
1845
+ const errorAriaLabel = errorEl.getAttribute("aria-label");
1846
+ if (errorText || errorAriaLabel) {
1847
+ hasErrorMessage = true;
1848
+ break;
1849
+ }
1850
+ }
1851
+ }
1852
+ if (!hasErrorMessage) {
1853
+ violations.push({
1854
+ id: "aria-invalid-without-message",
1855
+ description: 'Element with aria-invalid="true" should have associated error message via aria-describedby',
1856
+ element: el,
1857
+ impact: "serious"
1858
+ });
1859
+ }
1860
+ } else {
1861
+ violations.push({
1862
+ id: "aria-invalid-without-describedby",
1863
+ description: 'Element with aria-invalid="true" should have aria-describedby pointing to error message',
1864
+ element: el,
1865
+ impact: "serious"
1866
+ });
1867
+ }
1868
+ }
1869
+ }
1870
+ const requiredFields = element.querySelectorAll('[required], [aria-required="true"]');
1871
+ for (const field of Array.from(requiredFields)) {
1872
+ const label = element.querySelector(`label[for="${field.id}"]`) || field.closest("label");
1873
+ if (label) {
1874
+ const labelText = label.textContent || "";
1875
+ const hasRequiredIndicator = labelText.includes("*") || labelText.toLowerCase().includes("required") || field.hasAttribute("aria-required");
1876
+ if (!hasRequiredIndicator && !field.hasAttribute("aria-required")) {
1877
+ violations.push({
1878
+ id: "required-field-indicator",
1879
+ description: "Required field should have visual indicator (e.g., *) or aria-required attribute",
1880
+ element: field,
1881
+ impact: "moderate"
1882
+ });
1883
+ }
1884
+ }
1885
+ }
1886
+ return violations;
1887
+ }
1888
+ };
1889
+ export {
1890
+ A11yChecker
1891
+ };