@accesslint/core 0.3.13 → 0.3.15
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.cjs +5 -5
- package/dist/index.iife.js +5 -5
- package/dist/index.js +1022 -1015
- package/dist/rules/links/link-rules.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
let
|
|
1
|
+
let O = /* @__PURE__ */ new WeakMap();
|
|
2
2
|
function Se() {
|
|
3
|
-
|
|
3
|
+
O = /* @__PURE__ */ new WeakMap();
|
|
4
4
|
}
|
|
5
|
-
function he(
|
|
5
|
+
function he(t) {
|
|
6
6
|
var i;
|
|
7
|
-
const a =
|
|
7
|
+
const a = t.tagName.toLowerCase(), e = (i = t.getAttribute("type")) == null ? void 0 : i.toLowerCase();
|
|
8
8
|
switch (a) {
|
|
9
9
|
case "a":
|
|
10
|
-
return
|
|
10
|
+
return t.hasAttribute("href") ? "link" : null;
|
|
11
11
|
case "area":
|
|
12
|
-
return
|
|
12
|
+
return t.hasAttribute("href") ? "link" : null;
|
|
13
13
|
case "article":
|
|
14
14
|
return "article";
|
|
15
15
|
case "aside":
|
|
@@ -27,7 +27,7 @@ function he(e) {
|
|
|
27
27
|
case "figure":
|
|
28
28
|
return "figure";
|
|
29
29
|
case "footer":
|
|
30
|
-
return
|
|
30
|
+
return t.closest("article, aside, main, nav, section") ? null : "contentinfo";
|
|
31
31
|
case "form":
|
|
32
32
|
return "form";
|
|
33
33
|
case "h1":
|
|
@@ -38,13 +38,13 @@ function he(e) {
|
|
|
38
38
|
case "h6":
|
|
39
39
|
return "heading";
|
|
40
40
|
case "header":
|
|
41
|
-
return
|
|
41
|
+
return t.closest("article, aside, main, nav, section") ? null : "banner";
|
|
42
42
|
case "hr":
|
|
43
43
|
return "separator";
|
|
44
44
|
case "img":
|
|
45
|
-
return
|
|
45
|
+
return t.getAttribute("alt") === "" ? "presentation" : "img";
|
|
46
46
|
case "input":
|
|
47
|
-
switch (
|
|
47
|
+
switch (e) {
|
|
48
48
|
case "button":
|
|
49
49
|
case "image":
|
|
50
50
|
case "reset":
|
|
@@ -71,7 +71,7 @@ function he(e) {
|
|
|
71
71
|
return "textbox";
|
|
72
72
|
}
|
|
73
73
|
case "li":
|
|
74
|
-
return
|
|
74
|
+
return t.closest("ul, ol, menu") ? "listitem" : null;
|
|
75
75
|
case "main":
|
|
76
76
|
return "main";
|
|
77
77
|
case "math":
|
|
@@ -94,9 +94,9 @@ function he(e) {
|
|
|
94
94
|
case "progress":
|
|
95
95
|
return "progressbar";
|
|
96
96
|
case "section":
|
|
97
|
-
return
|
|
97
|
+
return t.hasAttribute("aria-label") || t.hasAttribute("aria-labelledby") ? "region" : null;
|
|
98
98
|
case "select":
|
|
99
|
-
return
|
|
99
|
+
return t.hasAttribute("multiple") || t.size > 1 ? "listbox" : "combobox";
|
|
100
100
|
case "summary":
|
|
101
101
|
return "button";
|
|
102
102
|
case "table":
|
|
@@ -117,69 +117,69 @@ function he(e) {
|
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
function q(
|
|
120
|
+
function q(t) {
|
|
121
121
|
var n;
|
|
122
|
-
const a =
|
|
122
|
+
const a = O.get(t);
|
|
123
123
|
if (a !== void 0) return a;
|
|
124
|
-
const i = ((n =
|
|
125
|
-
return
|
|
124
|
+
const i = ((n = t.getAttribute("role")) == null ? void 0 : n.trim().toLowerCase()) || null || he(t);
|
|
125
|
+
return O.set(t, i), i;
|
|
126
126
|
}
|
|
127
|
-
let
|
|
127
|
+
let B = /* @__PURE__ */ new WeakMap();
|
|
128
128
|
function ke() {
|
|
129
|
-
|
|
129
|
+
B = /* @__PURE__ */ new WeakMap();
|
|
130
130
|
}
|
|
131
|
-
function v(
|
|
132
|
-
const a =
|
|
131
|
+
function v(t) {
|
|
132
|
+
const a = B.get(t);
|
|
133
133
|
if (a !== void 0) return a;
|
|
134
|
-
const
|
|
135
|
-
return
|
|
134
|
+
const e = Ie(t);
|
|
135
|
+
return B.set(t, e), e;
|
|
136
136
|
}
|
|
137
|
-
function Ie(
|
|
138
|
-
var r, o, s, l,
|
|
139
|
-
const a =
|
|
137
|
+
function Ie(t) {
|
|
138
|
+
var r, o, s, l, h;
|
|
139
|
+
const a = t.getAttribute("aria-labelledby");
|
|
140
140
|
if (a) {
|
|
141
141
|
const c = a.split(/\s+/).map((u) => {
|
|
142
|
-
const
|
|
143
|
-
return
|
|
142
|
+
const p = t.ownerDocument.getElementById(u);
|
|
143
|
+
return p ? A(p).trim() : "";
|
|
144
144
|
}).filter(Boolean);
|
|
145
145
|
if (c.length) return c.join(" ");
|
|
146
146
|
}
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
if (
|
|
150
|
-
if (
|
|
151
|
-
const
|
|
147
|
+
const e = (r = t.getAttribute("aria-label")) == null ? void 0 : r.trim();
|
|
148
|
+
if (e) return e;
|
|
149
|
+
if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) {
|
|
150
|
+
if (t.id) {
|
|
151
|
+
const p = t.ownerDocument.querySelector(`label[for="${CSS.escape(t.id)}"]`), b = p ? A(p).trim() : "";
|
|
152
152
|
if (b) return b;
|
|
153
153
|
}
|
|
154
|
-
const c =
|
|
154
|
+
const c = t.closest("label"), u = c ? A(c).trim() : "";
|
|
155
155
|
if (u) return u;
|
|
156
156
|
}
|
|
157
|
-
const i = (o =
|
|
157
|
+
const i = (o = t.getAttribute("title")) == null ? void 0 : o.trim();
|
|
158
158
|
if (i) return i;
|
|
159
|
-
if (
|
|
160
|
-
const c = (s =
|
|
159
|
+
if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) {
|
|
160
|
+
const c = (s = t.getAttribute("placeholder")) == null ? void 0 : s.trim();
|
|
161
161
|
if (c) return c;
|
|
162
162
|
}
|
|
163
|
-
const n =
|
|
163
|
+
const n = t.tagName.toLowerCase();
|
|
164
164
|
if (n === "fieldset") {
|
|
165
|
-
const c =
|
|
165
|
+
const c = t.querySelector(":scope > legend");
|
|
166
166
|
if (c) {
|
|
167
167
|
const u = A(c).trim();
|
|
168
168
|
if (u) return u;
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
if (n === "table") {
|
|
172
|
-
const c =
|
|
172
|
+
const c = t.querySelector(":scope > caption");
|
|
173
173
|
if (c) {
|
|
174
174
|
const u = A(c).trim();
|
|
175
175
|
if (u) return u;
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
|
-
if (!(
|
|
179
|
-
const c = A(
|
|
178
|
+
if (!(t instanceof HTMLInputElement)) {
|
|
179
|
+
const c = A(t).trim();
|
|
180
180
|
if (c) return c;
|
|
181
181
|
}
|
|
182
|
-
return
|
|
182
|
+
return t instanceof HTMLImageElement || t instanceof HTMLAreaElement ? ((l = t.alt) == null ? void 0 : l.trim()) ?? "" : t instanceof HTMLInputElement && t.type === "image" ? ((h = t.alt) == null ? void 0 : h.trim()) ?? "" : "";
|
|
183
183
|
}
|
|
184
184
|
const Te = /* @__PURE__ */ new Set([
|
|
185
185
|
"alert",
|
|
@@ -265,53 +265,53 @@ const Te = /* @__PURE__ */ new Set([
|
|
|
265
265
|
"treegrid",
|
|
266
266
|
"treeitem"
|
|
267
267
|
]);
|
|
268
|
-
function Ee(
|
|
269
|
-
const a =
|
|
268
|
+
function Ee(t) {
|
|
269
|
+
const a = t.trim().toLowerCase().replace(/[\u201C\u201D\u2018\u2019\u00AB\u00BB]/g, "");
|
|
270
270
|
return Te.has(a);
|
|
271
271
|
}
|
|
272
|
-
function T(
|
|
273
|
-
let a =
|
|
272
|
+
function T(t) {
|
|
273
|
+
let a = t;
|
|
274
274
|
for (; a; ) {
|
|
275
275
|
if (pe(a)) return !0;
|
|
276
276
|
a = a.parentElement;
|
|
277
277
|
}
|
|
278
278
|
return !1;
|
|
279
279
|
}
|
|
280
|
-
let
|
|
280
|
+
let _ = /* @__PURE__ */ new WeakMap();
|
|
281
281
|
function Ce() {
|
|
282
|
-
|
|
282
|
+
_ = /* @__PURE__ */ new WeakMap();
|
|
283
283
|
}
|
|
284
|
-
function g(
|
|
285
|
-
const a =
|
|
284
|
+
function g(t) {
|
|
285
|
+
const a = _.get(t);
|
|
286
286
|
if (a !== void 0) return a;
|
|
287
|
-
let
|
|
288
|
-
return
|
|
287
|
+
let e;
|
|
288
|
+
return t.getAttribute("aria-hidden") === "true" || t instanceof HTMLElement && (t.hidden || t.style.display === "none") ? e = !0 : t.parentElement ? e = g(t.parentElement) : e = !1, _.set(t, e), e;
|
|
289
289
|
}
|
|
290
|
-
function pe(
|
|
291
|
-
if (
|
|
290
|
+
function pe(t) {
|
|
291
|
+
if (t.getAttribute("aria-hidden") === "true" || t instanceof HTMLElement && t.hidden) return !0;
|
|
292
292
|
if (typeof getComputedStyle == "function") {
|
|
293
|
-
const a = getComputedStyle(
|
|
293
|
+
const a = getComputedStyle(t);
|
|
294
294
|
if (a.display === "none" || a.visibility === "hidden") return !0;
|
|
295
|
-
} else if (
|
|
295
|
+
} else if (t instanceof HTMLElement && t.style.display === "none")
|
|
296
296
|
return !0;
|
|
297
297
|
return !1;
|
|
298
298
|
}
|
|
299
|
-
function A(
|
|
300
|
-
var
|
|
299
|
+
function A(t) {
|
|
300
|
+
var e, i, n, r, o;
|
|
301
301
|
let a = "";
|
|
302
|
-
for (const s of
|
|
302
|
+
for (const s of t.childNodes)
|
|
303
303
|
if (s.nodeType === 3)
|
|
304
304
|
a += s.textContent ?? "";
|
|
305
305
|
else if (s.nodeType === 1) {
|
|
306
306
|
const l = s;
|
|
307
307
|
if (!pe(l)) {
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
308
|
+
const h = (e = l.tagName) == null ? void 0 : e.toLowerCase();
|
|
309
|
+
if (h === "img" || h === "area") {
|
|
310
310
|
const c = l.getAttribute("aria-labelledby");
|
|
311
311
|
if (c) {
|
|
312
|
-
const u = c.split(/\s+/).map((
|
|
312
|
+
const u = c.split(/\s+/).map((p) => {
|
|
313
313
|
var b, f;
|
|
314
|
-
return ((f = (b = l.ownerDocument.getElementById(
|
|
314
|
+
return ((f = (b = l.ownerDocument.getElementById(p)) == null ? void 0 : b.textContent) == null ? void 0 : f.trim()) ?? "";
|
|
315
315
|
}).filter(Boolean);
|
|
316
316
|
if (u.length) {
|
|
317
317
|
a += u.join(" ");
|
|
@@ -319,7 +319,7 @@ function A(e) {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
a += ((i = l.getAttribute("aria-label")) == null ? void 0 : i.trim()) ?? l.getAttribute("alt") ?? ((n = l.getAttribute("title")) == null ? void 0 : n.trim()) ?? "";
|
|
322
|
-
} else if (
|
|
322
|
+
} else if (h === "svg") {
|
|
323
323
|
const c = (r = l.getAttribute("aria-label")) == null ? void 0 : r.trim();
|
|
324
324
|
if (c)
|
|
325
325
|
a += c;
|
|
@@ -332,12 +332,12 @@ function A(e) {
|
|
|
332
332
|
}
|
|
333
333
|
return a;
|
|
334
334
|
}
|
|
335
|
-
let
|
|
335
|
+
let P = /* @__PURE__ */ new WeakMap();
|
|
336
336
|
function Le() {
|
|
337
|
-
|
|
337
|
+
P = /* @__PURE__ */ new WeakMap();
|
|
338
338
|
}
|
|
339
|
-
function qe(
|
|
340
|
-
return
|
|
339
|
+
function qe(t) {
|
|
340
|
+
return t.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
341
341
|
}
|
|
342
342
|
const Re = [
|
|
343
343
|
"data-testid",
|
|
@@ -349,29 +349,29 @@ const Re = [
|
|
|
349
349
|
"for",
|
|
350
350
|
"aria-label"
|
|
351
351
|
];
|
|
352
|
-
function Ne(
|
|
353
|
-
const a =
|
|
352
|
+
function Ne(t) {
|
|
353
|
+
const a = t.tagName.toLowerCase();
|
|
354
354
|
for (const i of Re) {
|
|
355
|
-
const n =
|
|
355
|
+
const n = t.getAttribute(i);
|
|
356
356
|
if (n != null && n.length > 0 && n.length < 100)
|
|
357
357
|
return `${a}[${i}="${qe(n)}"]`;
|
|
358
358
|
}
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
359
|
+
const e = t.parentElement;
|
|
360
|
+
if (e) {
|
|
361
361
|
let i = 0, n = 0;
|
|
362
|
-
for (let r = 0; r <
|
|
363
|
-
|
|
362
|
+
for (let r = 0; r < e.children.length; r++)
|
|
363
|
+
e.children[r].tagName === t.tagName && (i++, e.children[r] === t && (n = i));
|
|
364
364
|
if (i > 1)
|
|
365
365
|
return `${a}:nth-of-type(${n})`;
|
|
366
366
|
}
|
|
367
367
|
return a;
|
|
368
368
|
}
|
|
369
|
-
function H(
|
|
370
|
-
if (
|
|
371
|
-
const a =
|
|
372
|
-
let n =
|
|
373
|
-
for (; n && n !==
|
|
374
|
-
if (n !==
|
|
369
|
+
function H(t) {
|
|
370
|
+
if (t.id) return `#${CSS.escape(t.id)}`;
|
|
371
|
+
const a = t.getRootNode(), e = a instanceof ShadowRoot ? null : a.documentElement, i = [];
|
|
372
|
+
let n = t;
|
|
373
|
+
for (; n && n !== e; ) {
|
|
374
|
+
if (n !== t && n.id) {
|
|
375
375
|
i.unshift(`#${CSS.escape(n.id)}`);
|
|
376
376
|
break;
|
|
377
377
|
}
|
|
@@ -379,7 +379,7 @@ function H(e) {
|
|
|
379
379
|
const r = i.join(" > ");
|
|
380
380
|
try {
|
|
381
381
|
const o = a.querySelectorAll(r);
|
|
382
|
-
if (o.length === 1 && o[0] ===
|
|
382
|
+
if (o.length === 1 && o[0] === t) return r;
|
|
383
383
|
} catch {
|
|
384
384
|
}
|
|
385
385
|
}
|
|
@@ -387,38 +387,38 @@ function H(e) {
|
|
|
387
387
|
}
|
|
388
388
|
return i.join(" > ");
|
|
389
389
|
}
|
|
390
|
-
function m(
|
|
390
|
+
function m(t) {
|
|
391
391
|
var r;
|
|
392
|
-
const a =
|
|
392
|
+
const a = P.get(t);
|
|
393
393
|
if (a !== void 0) return a;
|
|
394
|
-
const
|
|
395
|
-
let i =
|
|
394
|
+
const e = [];
|
|
395
|
+
let i = t;
|
|
396
396
|
for (; i; ) {
|
|
397
397
|
const o = i.getRootNode();
|
|
398
398
|
if (o instanceof ShadowRoot)
|
|
399
|
-
|
|
399
|
+
e.unshift({ selector: H(i), delimiter: " >>> " }), i = o.host;
|
|
400
400
|
else {
|
|
401
401
|
const s = (r = o.defaultView) == null ? void 0 : r.frameElement;
|
|
402
402
|
if (s)
|
|
403
|
-
|
|
403
|
+
e.unshift({ selector: H(i), delimiter: " >>>iframe> " }), i = s;
|
|
404
404
|
else {
|
|
405
|
-
|
|
405
|
+
e.unshift({ selector: H(i), delimiter: "" });
|
|
406
406
|
break;
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
|
-
const n =
|
|
411
|
-
return
|
|
410
|
+
const n = e.map((o, s) => (s === 0 ? "" : o.delimiter) + o.selector).join("");
|
|
411
|
+
return P.set(t, n), n;
|
|
412
412
|
}
|
|
413
|
-
function Ri(
|
|
414
|
-
const a = [],
|
|
415
|
-
let i =
|
|
413
|
+
function Ri(t) {
|
|
414
|
+
const a = [], e = [];
|
|
415
|
+
let i = t;
|
|
416
416
|
for (; i; ) {
|
|
417
417
|
const r = i.indexOf(" >>>iframe> "), o = i.indexOf(" >>> ");
|
|
418
418
|
if (r !== -1 && (o === -1 || r <= o))
|
|
419
|
-
a.push(i.slice(0, r).trim()),
|
|
419
|
+
a.push(i.slice(0, r).trim()), e.push("iframe"), i = i.slice(r + 12);
|
|
420
420
|
else if (o !== -1)
|
|
421
|
-
a.push(i.slice(0, o).trim()),
|
|
421
|
+
a.push(i.slice(0, o).trim()), e.push("shadow"), i = i.slice(o + 5);
|
|
422
422
|
else {
|
|
423
423
|
a.push(i.trim());
|
|
424
424
|
break;
|
|
@@ -429,7 +429,7 @@ function Ri(e) {
|
|
|
429
429
|
const o = n.querySelector(a[r]);
|
|
430
430
|
if (!o) return null;
|
|
431
431
|
if (r < a.length - 1)
|
|
432
|
-
if (
|
|
432
|
+
if (e[r] === "iframe") {
|
|
433
433
|
const s = o.contentDocument;
|
|
434
434
|
if (!s) return null;
|
|
435
435
|
n = s;
|
|
@@ -443,8 +443,8 @@ function Ri(e) {
|
|
|
443
443
|
}
|
|
444
444
|
return null;
|
|
445
445
|
}
|
|
446
|
-
function d(
|
|
447
|
-
const a =
|
|
446
|
+
function d(t) {
|
|
447
|
+
const a = t.outerHTML;
|
|
448
448
|
return a.length > 200 ? a.slice(0, 200) + "..." : a;
|
|
449
449
|
}
|
|
450
450
|
const $e = /* @__PURE__ */ new Set([
|
|
@@ -501,7 +501,7 @@ const $e = /* @__PURE__ */ new Set([
|
|
|
501
501
|
"aria-valuemin",
|
|
502
502
|
"aria-valuenow",
|
|
503
503
|
"aria-valuetext"
|
|
504
|
-
]),
|
|
504
|
+
]), X = /* @__PURE__ */ new Set([
|
|
505
505
|
"aria-atomic",
|
|
506
506
|
"aria-busy",
|
|
507
507
|
"aria-disabled",
|
|
@@ -512,7 +512,7 @@ const $e = /* @__PURE__ */ new Set([
|
|
|
512
512
|
"aria-multiselectable",
|
|
513
513
|
"aria-readonly",
|
|
514
514
|
"aria-required"
|
|
515
|
-
]),
|
|
515
|
+
]), K = /* @__PURE__ */ new Set(["aria-checked", "aria-pressed"]), Me = /* @__PURE__ */ new Set([
|
|
516
516
|
"aria-colcount",
|
|
517
517
|
"aria-colindex",
|
|
518
518
|
"aria-colspan",
|
|
@@ -526,7 +526,7 @@ const $e = /* @__PURE__ */ new Set([
|
|
|
526
526
|
"aria-valuemax",
|
|
527
527
|
"aria-valuemin",
|
|
528
528
|
"aria-valuenow"
|
|
529
|
-
]),
|
|
529
|
+
]), J = {
|
|
530
530
|
"aria-autocomplete": /* @__PURE__ */ new Set(["inline", "list", "both", "none"]),
|
|
531
531
|
"aria-expanded": /* @__PURE__ */ new Set(["true", "false", "undefined"]),
|
|
532
532
|
"aria-current": /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"]),
|
|
@@ -537,7 +537,7 @@ const $e = /* @__PURE__ */ new Set([
|
|
|
537
537
|
"aria-orientation": /* @__PURE__ */ new Set(["horizontal", "vertical", "undefined"]),
|
|
538
538
|
"aria-relevant": /* @__PURE__ */ new Set(["additions", "all", "removals", "text"]),
|
|
539
539
|
"aria-sort": /* @__PURE__ */ new Set(["ascending", "descending", "none", "other"])
|
|
540
|
-
},
|
|
540
|
+
}, Q = /* @__PURE__ */ new Set([
|
|
541
541
|
"caption",
|
|
542
542
|
"code",
|
|
543
543
|
"deletion",
|
|
@@ -619,11 +619,11 @@ let C = null, L = null;
|
|
|
619
619
|
function We() {
|
|
620
620
|
C = null, L = null;
|
|
621
621
|
}
|
|
622
|
-
function
|
|
622
|
+
function G(t) {
|
|
623
623
|
var n;
|
|
624
|
-
if (L && (C == null ? void 0 : C.deref()) ===
|
|
625
|
-
const a = [],
|
|
626
|
-
for (const r of
|
|
624
|
+
if (L && (C == null ? void 0 : C.deref()) === t) return L;
|
|
625
|
+
const a = [], e = [], i = [];
|
|
626
|
+
for (const r of t.querySelectorAll("*")) {
|
|
627
627
|
let o = !1;
|
|
628
628
|
for (const c of r.attributes)
|
|
629
629
|
if (c.name.startsWith("aria-")) {
|
|
@@ -632,10 +632,10 @@ function U(e) {
|
|
|
632
632
|
}
|
|
633
633
|
if (!o) continue;
|
|
634
634
|
let s, l;
|
|
635
|
-
const
|
|
635
|
+
const h = () => (s === void 0 && (s = m(r), l = d(r)), { selector: s, html: l });
|
|
636
636
|
for (const c of r.attributes)
|
|
637
637
|
if (c.name.startsWith("aria-") && !$e.has(c.name)) {
|
|
638
|
-
const u =
|
|
638
|
+
const u = h();
|
|
639
639
|
a.push({
|
|
640
640
|
ruleId: "aria-valid-attr",
|
|
641
641
|
selector: u.selector,
|
|
@@ -648,57 +648,57 @@ function U(e) {
|
|
|
648
648
|
for (const c of r.attributes) {
|
|
649
649
|
if (!c.name.startsWith("aria-")) continue;
|
|
650
650
|
const u = c.value.trim();
|
|
651
|
-
if (!(u === "" && !
|
|
652
|
-
if (
|
|
651
|
+
if (!(u === "" && !X.has(c.name) && !K.has(c.name))) {
|
|
652
|
+
if (X.has(c.name)) {
|
|
653
653
|
if (u !== "true" && u !== "false") {
|
|
654
|
-
const
|
|
655
|
-
|
|
654
|
+
const p = h();
|
|
655
|
+
e.push({
|
|
656
656
|
ruleId: "aria-valid-attr-value",
|
|
657
|
-
selector:
|
|
658
|
-
html:
|
|
657
|
+
selector: p.selector,
|
|
658
|
+
html: p.html,
|
|
659
659
|
impact: "critical",
|
|
660
660
|
message: `${c.name} must be "true" or "false", got "${u}".`
|
|
661
661
|
});
|
|
662
662
|
}
|
|
663
|
-
} else if (
|
|
663
|
+
} else if (K.has(c.name)) {
|
|
664
664
|
if (u !== "true" && u !== "false" && u !== "mixed") {
|
|
665
|
-
const
|
|
666
|
-
|
|
665
|
+
const p = h();
|
|
666
|
+
e.push({
|
|
667
667
|
ruleId: "aria-valid-attr-value",
|
|
668
|
-
selector:
|
|
669
|
-
html:
|
|
668
|
+
selector: p.selector,
|
|
669
|
+
html: p.html,
|
|
670
670
|
impact: "critical",
|
|
671
671
|
message: `${c.name} must be "true", "false", or "mixed", got "${u}".`
|
|
672
672
|
});
|
|
673
673
|
}
|
|
674
674
|
} else if (Me.has(c.name)) {
|
|
675
675
|
if (u === "" || !/^-?\d+$/.test(u)) {
|
|
676
|
-
const
|
|
677
|
-
|
|
676
|
+
const p = h();
|
|
677
|
+
e.push({
|
|
678
678
|
ruleId: "aria-valid-attr-value",
|
|
679
|
-
selector:
|
|
680
|
-
html:
|
|
679
|
+
selector: p.selector,
|
|
680
|
+
html: p.html,
|
|
681
681
|
impact: "critical",
|
|
682
682
|
message: `${c.name} must be an integer, got "${u}".`
|
|
683
683
|
});
|
|
684
684
|
}
|
|
685
685
|
} else if (He.has(c.name)) {
|
|
686
686
|
if (u === "" || isNaN(Number(u))) {
|
|
687
|
-
const
|
|
688
|
-
|
|
687
|
+
const p = h();
|
|
688
|
+
e.push({
|
|
689
689
|
ruleId: "aria-valid-attr-value",
|
|
690
|
-
selector:
|
|
691
|
-
html:
|
|
690
|
+
selector: p.selector,
|
|
691
|
+
html: p.html,
|
|
692
692
|
impact: "critical",
|
|
693
693
|
message: `${c.name} must be a number, got "${u}".`
|
|
694
694
|
});
|
|
695
695
|
}
|
|
696
|
-
} else if (
|
|
697
|
-
const
|
|
698
|
-
for (const b of
|
|
699
|
-
if (!
|
|
700
|
-
const f =
|
|
701
|
-
|
|
696
|
+
} else if (J[c.name]) {
|
|
697
|
+
const p = u.split(/\s+/);
|
|
698
|
+
for (const b of p)
|
|
699
|
+
if (!J[c.name].has(b)) {
|
|
700
|
+
const f = h();
|
|
701
|
+
e.push({
|
|
702
702
|
ruleId: "aria-valid-attr-value",
|
|
703
703
|
selector: f.selector,
|
|
704
704
|
html: f.html,
|
|
@@ -713,9 +713,9 @@ function U(e) {
|
|
|
713
713
|
if (!g(r)) {
|
|
714
714
|
const c = (n = r.getAttribute("role")) == null ? void 0 : n.trim().toLowerCase(), u = r.tagName.toLowerCase();
|
|
715
715
|
if (!c && De[u]) {
|
|
716
|
-
const
|
|
717
|
-
if (
|
|
718
|
-
const f =
|
|
716
|
+
const p = r.hasAttribute("aria-label"), b = r.hasAttribute("aria-labelledby");
|
|
717
|
+
if (p || b) {
|
|
718
|
+
const f = h();
|
|
719
719
|
i.push({
|
|
720
720
|
ruleId: "aria-prohibited-attr",
|
|
721
721
|
selector: f.selector,
|
|
@@ -725,26 +725,26 @@ function U(e) {
|
|
|
725
725
|
});
|
|
726
726
|
}
|
|
727
727
|
} else if (c) {
|
|
728
|
-
if (
|
|
728
|
+
if (Q.has(c)) {
|
|
729
729
|
const b = r.hasAttribute("aria-label"), f = r.hasAttribute("aria-labelledby");
|
|
730
730
|
if (b || f) {
|
|
731
|
-
const
|
|
731
|
+
const y = h();
|
|
732
732
|
i.push({
|
|
733
733
|
ruleId: "aria-prohibited-attr",
|
|
734
|
-
selector:
|
|
735
|
-
html:
|
|
734
|
+
selector: y.selector,
|
|
735
|
+
html: y.html,
|
|
736
736
|
impact: "serious",
|
|
737
737
|
message: `aria-label and aria-labelledby are prohibited on role "${c}".`
|
|
738
738
|
});
|
|
739
739
|
}
|
|
740
740
|
}
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
741
|
+
const p = Fe[c];
|
|
742
|
+
if (p) {
|
|
743
743
|
for (const b of r.attributes)
|
|
744
|
-
if (b.name.startsWith("aria-") &&
|
|
745
|
-
if ((b.name === "aria-label" || b.name === "aria-labelledby") &&
|
|
744
|
+
if (b.name.startsWith("aria-") && p.has(b.name)) {
|
|
745
|
+
if ((b.name === "aria-label" || b.name === "aria-labelledby") && Q.has(c))
|
|
746
746
|
continue;
|
|
747
|
-
const f =
|
|
747
|
+
const f = h();
|
|
748
748
|
i.push({
|
|
749
749
|
ruleId: "aria-prohibited-attr",
|
|
750
750
|
selector: f.selector,
|
|
@@ -757,28 +757,28 @@ function U(e) {
|
|
|
757
757
|
}
|
|
758
758
|
}
|
|
759
759
|
}
|
|
760
|
-
return C = new WeakRef(
|
|
760
|
+
return C = new WeakRef(t), L = { validAttr: a, validAttrValue: e, prohibitedAttr: i }, L;
|
|
761
761
|
}
|
|
762
|
-
let
|
|
762
|
+
let j = /* @__PURE__ */ new WeakMap(), V = /* @__PURE__ */ new WeakMap(), z = /* @__PURE__ */ new WeakMap();
|
|
763
763
|
function Oe() {
|
|
764
|
-
|
|
764
|
+
j = /* @__PURE__ */ new WeakMap(), V = /* @__PURE__ */ new WeakMap(), z = /* @__PURE__ */ new WeakMap();
|
|
765
765
|
}
|
|
766
|
-
function
|
|
767
|
-
let a =
|
|
768
|
-
return a || (a = getComputedStyle(
|
|
766
|
+
function w(t) {
|
|
767
|
+
let a = j.get(t);
|
|
768
|
+
return a || (a = getComputedStyle(t), j.set(t, a), a);
|
|
769
769
|
}
|
|
770
|
-
function R(
|
|
771
|
-
const [i, n, r] = [
|
|
770
|
+
function R(t, a, e) {
|
|
771
|
+
const [i, n, r] = [t, a, e].map((o) => {
|
|
772
772
|
const s = o / 255;
|
|
773
773
|
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
774
774
|
});
|
|
775
775
|
return 0.2126 * i + 0.7152 * n + 0.0722 * r;
|
|
776
776
|
}
|
|
777
|
-
function ge(
|
|
778
|
-
const
|
|
779
|
-
return (
|
|
777
|
+
function ge(t, a) {
|
|
778
|
+
const e = Math.max(t, a), i = Math.min(t, a);
|
|
779
|
+
return (e + 0.05) / (i + 0.05);
|
|
780
780
|
}
|
|
781
|
-
const
|
|
781
|
+
const Z = {
|
|
782
782
|
black: [0, 0, 0],
|
|
783
783
|
white: [255, 255, 255],
|
|
784
784
|
red: [255, 0, 0],
|
|
@@ -798,37 +798,37 @@ const Q = {
|
|
|
798
798
|
lime: [0, 255, 0],
|
|
799
799
|
olive: [128, 128, 0]
|
|
800
800
|
};
|
|
801
|
-
function N(
|
|
802
|
-
const a =
|
|
803
|
-
if (
|
|
804
|
-
const
|
|
805
|
-
if (
|
|
806
|
-
return [parseInt(
|
|
801
|
+
function N(t) {
|
|
802
|
+
const a = t.trim().toLowerCase();
|
|
803
|
+
if (Z[a]) return Z[a];
|
|
804
|
+
const e = a.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
805
|
+
if (e)
|
|
806
|
+
return [parseInt(e[1] + e[1], 16), parseInt(e[2] + e[2], 16), parseInt(e[3] + e[3], 16)];
|
|
807
807
|
const i = a.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
808
808
|
if (i)
|
|
809
809
|
return [parseInt(i[1], 16), parseInt(i[2], 16), parseInt(i[3], 16)];
|
|
810
|
-
const n =
|
|
810
|
+
const n = t.match(
|
|
811
811
|
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+)?\s*\)/
|
|
812
812
|
);
|
|
813
813
|
if (n)
|
|
814
814
|
return [parseInt(n[1]), parseInt(n[2]), parseInt(n[3])];
|
|
815
|
-
const r =
|
|
815
|
+
const r = t.match(
|
|
816
816
|
/rgba?\(\s*(\d+)\s+(\d+)\s+(\d+)\s*(?:\/\s*[\d.]+%?)?\s*\)/
|
|
817
817
|
);
|
|
818
818
|
return r ? [parseInt(r[1]), parseInt(r[2]), parseInt(r[3])] : null;
|
|
819
819
|
}
|
|
820
|
-
function Be(
|
|
821
|
-
const a =
|
|
820
|
+
function Be(t) {
|
|
821
|
+
const a = V.get(t);
|
|
822
822
|
if (a !== void 0) return a;
|
|
823
|
-
const
|
|
824
|
-
return
|
|
823
|
+
const e = _e(t);
|
|
824
|
+
return V.set(t, e), e;
|
|
825
825
|
}
|
|
826
|
-
function _e(
|
|
827
|
-
let a =
|
|
826
|
+
function _e(t) {
|
|
827
|
+
let a = t;
|
|
828
828
|
for (; a; ) {
|
|
829
|
-
const
|
|
829
|
+
const e = w(a), i = e.backgroundImage;
|
|
830
830
|
if (i && i !== "none" && i !== "initial") return null;
|
|
831
|
-
const n =
|
|
831
|
+
const n = e.backgroundColor;
|
|
832
832
|
if (n === "transparent" || n === "rgba(0, 0, 0, 0)" || n === "rgba(0 0 0 / 0)") {
|
|
833
833
|
a = a.parentElement;
|
|
834
834
|
continue;
|
|
@@ -843,59 +843,59 @@ function _e(e) {
|
|
|
843
843
|
return [255, 255, 255];
|
|
844
844
|
}
|
|
845
845
|
const Pe = /* @__PURE__ */ new Set(["IMG", "PICTURE", "VIDEO", "SVG"]);
|
|
846
|
-
function je(
|
|
847
|
-
const a =
|
|
846
|
+
function je(t) {
|
|
847
|
+
const a = z.get(t);
|
|
848
848
|
if (a !== void 0) return a;
|
|
849
|
-
const
|
|
850
|
-
return
|
|
849
|
+
const e = Ve(t);
|
|
850
|
+
return z.set(t, e), e;
|
|
851
851
|
}
|
|
852
|
-
function Ve(
|
|
853
|
-
let a =
|
|
852
|
+
function Ve(t) {
|
|
853
|
+
let a = t, e = !1;
|
|
854
854
|
for (; a; ) {
|
|
855
|
-
const i =
|
|
856
|
-
if ((i === "absolute" || i === "fixed") && (
|
|
855
|
+
const i = w(a).position;
|
|
856
|
+
if ((i === "absolute" || i === "fixed") && (e = !0), a !== t && i !== "static") {
|
|
857
857
|
for (const n of a.children)
|
|
858
|
-
if (!(n ===
|
|
859
|
-
if (
|
|
860
|
-
const r =
|
|
858
|
+
if (!(n === t || n.contains(t)) && Pe.has(n.tagName)) {
|
|
859
|
+
if (e) return !0;
|
|
860
|
+
const r = w(n).position;
|
|
861
861
|
if (r === "absolute" || r === "fixed") return !0;
|
|
862
862
|
}
|
|
863
|
-
if (
|
|
863
|
+
if (e) break;
|
|
864
864
|
}
|
|
865
865
|
a = a.parentElement;
|
|
866
866
|
}
|
|
867
867
|
return !1;
|
|
868
868
|
}
|
|
869
|
-
function ze(
|
|
870
|
-
const a = parseFloat(
|
|
871
|
-
return
|
|
869
|
+
function ze(t) {
|
|
870
|
+
const a = parseFloat(t);
|
|
871
|
+
return t.endsWith("pt") ? a * (4 / 3) : a;
|
|
872
872
|
}
|
|
873
|
-
function Ue(
|
|
874
|
-
const a =
|
|
875
|
-
return
|
|
873
|
+
function Ue(t) {
|
|
874
|
+
const a = w(t), e = ze(a.fontSize), i = parseInt(a.fontWeight) || (a.fontWeight === "bold" ? 700 : 400);
|
|
875
|
+
return e >= 23.5 || e >= 18.5 && i >= 700;
|
|
876
876
|
}
|
|
877
|
-
function D(
|
|
877
|
+
function D(t) {
|
|
878
878
|
var r, o;
|
|
879
|
-
const a = [],
|
|
880
|
-
if (
|
|
881
|
-
const s =
|
|
879
|
+
const a = [], e = t.closest("a");
|
|
880
|
+
if (e) {
|
|
881
|
+
const s = e.getAttribute("href");
|
|
882
882
|
s && a.push(`Link href: ${s}`);
|
|
883
883
|
}
|
|
884
|
-
const i =
|
|
884
|
+
const i = t.closest("figure");
|
|
885
885
|
if (i) {
|
|
886
886
|
const s = i.querySelector("figcaption");
|
|
887
887
|
(r = s == null ? void 0 : s.textContent) != null && r.trim() && a.push(`Figcaption: ${s.textContent.trim().slice(0, 100)}`);
|
|
888
888
|
}
|
|
889
|
-
const n =
|
|
890
|
-
if (n && n !==
|
|
891
|
-
const s =
|
|
889
|
+
const n = t.parentElement;
|
|
890
|
+
if (n && n !== e) {
|
|
891
|
+
const s = t instanceof HTMLImageElement && t.alt || "", l = (o = n.textContent) == null ? void 0 : o.replace(s, "").trim().slice(0, 100);
|
|
892
892
|
l && a.push(`Adjacent text: ${l}`);
|
|
893
893
|
}
|
|
894
894
|
return a.length > 0 ? a.join(`
|
|
895
895
|
`) : void 0;
|
|
896
896
|
}
|
|
897
|
-
function
|
|
898
|
-
let a =
|
|
897
|
+
function ee(t) {
|
|
898
|
+
let a = t;
|
|
899
899
|
for (; a; ) {
|
|
900
900
|
if (a instanceof HTMLElement && a.style.visibility === "hidden") return !0;
|
|
901
901
|
a = a.parentElement;
|
|
@@ -909,63 +909,63 @@ const Ge = {
|
|
|
909
909
|
description: `Images must have alternate text. Add an alt attribute to <img> elements. Decorative images may use an empty alt attribute (alt=""), role='none', or role='presentation'.`,
|
|
910
910
|
guidance: "Every image needs an alt attribute. For informative images, describe the content or function concisely. For decorative images (backgrounds, spacers, purely visual flourishes), use alt='' to hide them from screen readers. Never omit alt entirely—screen readers may read the filename instead.",
|
|
911
911
|
prompt: "Describe what alt text to add. If the image appears decorative based on context (spacer, background, icon next to text that already describes it), recommend alt=''. Otherwise suggest descriptive alt text based on the src or surrounding context.",
|
|
912
|
-
run(
|
|
912
|
+
run(t) {
|
|
913
913
|
const a = [];
|
|
914
|
-
for (const
|
|
915
|
-
if (g(
|
|
916
|
-
const i =
|
|
914
|
+
for (const e of t.querySelectorAll("img")) {
|
|
915
|
+
if (g(e) || ee(e)) continue;
|
|
916
|
+
const i = e.getAttribute("role");
|
|
917
917
|
if (i === "presentation" || i === "none") {
|
|
918
|
-
const r =
|
|
918
|
+
const r = e.getAttribute("tabindex");
|
|
919
919
|
if (!r || r === "-1") continue;
|
|
920
920
|
}
|
|
921
|
-
const n =
|
|
921
|
+
const n = e.getAttribute("alt");
|
|
922
922
|
if (n !== null && n.trim() === "" && n !== "") {
|
|
923
923
|
a.push({
|
|
924
924
|
ruleId: "img-alt",
|
|
925
|
-
selector: m(
|
|
926
|
-
html: d(
|
|
925
|
+
selector: m(e),
|
|
926
|
+
html: d(e),
|
|
927
927
|
impact: "critical",
|
|
928
928
|
message: 'Image has whitespace-only alt text. Use alt="" for decorative images or provide descriptive text.',
|
|
929
|
-
context: D(
|
|
929
|
+
context: D(e)
|
|
930
930
|
});
|
|
931
931
|
continue;
|
|
932
932
|
}
|
|
933
|
-
!
|
|
933
|
+
!e.hasAttribute("alt") && !v(e) && a.push({
|
|
934
934
|
ruleId: "img-alt",
|
|
935
|
-
selector: m(
|
|
936
|
-
html: d(
|
|
935
|
+
selector: m(e),
|
|
936
|
+
html: d(e),
|
|
937
937
|
impact: "critical",
|
|
938
938
|
message: "Image element missing alt attribute.",
|
|
939
|
-
context: D(
|
|
939
|
+
context: D(e)
|
|
940
940
|
});
|
|
941
941
|
}
|
|
942
|
-
for (const
|
|
943
|
-
g(
|
|
942
|
+
for (const e of t.querySelectorAll('[role="img"]:not(img):not(svg)'))
|
|
943
|
+
g(e) || ee(e) || v(e) || a.push({
|
|
944
944
|
ruleId: "img-alt",
|
|
945
|
-
selector: m(
|
|
946
|
-
html: d(
|
|
945
|
+
selector: m(e),
|
|
946
|
+
html: d(e),
|
|
947
947
|
impact: "critical",
|
|
948
948
|
message: 'Element with role="img" has no accessible name. Add aria-label or aria-labelledby.',
|
|
949
|
-
context: D(
|
|
949
|
+
context: D(e)
|
|
950
950
|
});
|
|
951
951
|
return a;
|
|
952
952
|
}
|
|
953
953
|
};
|
|
954
|
-
function Ye(
|
|
954
|
+
function Ye(t) {
|
|
955
955
|
var r, o, s;
|
|
956
|
-
const a =
|
|
956
|
+
const a = t.getAttribute("aria-labelledby");
|
|
957
957
|
if (a) {
|
|
958
|
-
const l = a.split(/\s+/).map((
|
|
958
|
+
const l = a.split(/\s+/).map((h) => {
|
|
959
959
|
var c, u;
|
|
960
|
-
return ((u = (c =
|
|
960
|
+
return ((u = (c = t.ownerDocument.getElementById(h)) == null ? void 0 : c.textContent) == null ? void 0 : u.trim()) ?? "";
|
|
961
961
|
}).filter(Boolean);
|
|
962
962
|
if (l.length) return l.join(" ");
|
|
963
963
|
}
|
|
964
|
-
const
|
|
965
|
-
if (
|
|
966
|
-
const i =
|
|
964
|
+
const e = (r = t.getAttribute("aria-label")) == null ? void 0 : r.trim();
|
|
965
|
+
if (e) return e;
|
|
966
|
+
const i = t.querySelector("title");
|
|
967
967
|
if ((o = i == null ? void 0 : i.textContent) != null && o.trim()) return i.textContent.trim();
|
|
968
|
-
const n = (s =
|
|
968
|
+
const n = (s = t.getAttribute("title")) == null ? void 0 : s.trim();
|
|
969
969
|
return n || "";
|
|
970
970
|
}
|
|
971
971
|
const Xe = {
|
|
@@ -975,9 +975,9 @@ const Xe = {
|
|
|
975
975
|
description: "SVG elements with an img, graphics-document, or graphics-symbol role must have an accessible name via a <title> element, aria-label, or aria-labelledby.",
|
|
976
976
|
guidance: "Inline SVGs with role='img' need accessible names. Add a <title> element as the first child of the SVG (screen readers will announce it), or use aria-label on the SVG element. For complex SVGs, use aria-labelledby referencing both a <title> and <desc> element. Decorative SVGs should use aria-hidden='true' instead.",
|
|
977
977
|
prompt: "Based on the SVG content or context, suggest either adding aria-label with a description, or if decorative, replacing role='img' with aria-hidden='true'.",
|
|
978
|
-
run(
|
|
979
|
-
const a = [],
|
|
980
|
-
for (const i of
|
|
978
|
+
run(t) {
|
|
979
|
+
const a = [], e = 'svg[role="img"], [role="graphics-document"], [role="graphics-symbol"]';
|
|
980
|
+
for (const i of t.querySelectorAll(e)) {
|
|
981
981
|
if (g(i)) continue;
|
|
982
982
|
if (!Ye(i)) {
|
|
983
983
|
const r = i.getAttribute("role");
|
|
@@ -999,13 +999,13 @@ const Xe = {
|
|
|
999
999
|
description: 'Image inputs (<input type="image">) must have alternate text via alt, aria-label, or aria-labelledby. The text should describe the button action, not the image.',
|
|
1000
1000
|
guidance: "Image buttons (<input type='image'>) must have alternate text via alt, aria-label, or aria-labelledby. The text should describe the button action, not the image.",
|
|
1001
1001
|
prompt: "Based on the src attribute or form context, suggest alt text describing the button's action (e.g., 'Submit', 'Search', 'Go').",
|
|
1002
|
-
run(
|
|
1002
|
+
run(t) {
|
|
1003
1003
|
const a = [];
|
|
1004
|
-
for (const
|
|
1005
|
-
g(
|
|
1004
|
+
for (const e of t.querySelectorAll('input[type="image"]'))
|
|
1005
|
+
g(e) || v(e) || a.push({
|
|
1006
1006
|
ruleId: "input-image-alt",
|
|
1007
|
-
selector: m(
|
|
1008
|
-
html: d(
|
|
1007
|
+
selector: m(e),
|
|
1008
|
+
html: d(e),
|
|
1009
1009
|
impact: "critical",
|
|
1010
1010
|
message: "Image input missing alt text."
|
|
1011
1011
|
});
|
|
@@ -1019,15 +1019,15 @@ const Xe = {
|
|
|
1019
1019
|
description: "Image alt text should not duplicate adjacent link or button text. When alt text repeats surrounding text, screen reader users hear the same information twice.",
|
|
1020
1020
|
guidance: "When an image is inside a link or button that also has text, make the alt text complementary rather than identical. If the image is purely decorative in that context, use alt='' to avoid repetition.",
|
|
1021
1021
|
prompt: "The image alt text is identical to the text already visible in the parent link or button. Screen reader users hear the same words twice. If the image is decorative in this context (e.g. an icon next to a label), recommend alt=''. Otherwise suggest brief complementary alt text that adds information the visible text doesn't convey — for example what the image depicts, not what the link says.",
|
|
1022
|
-
run(
|
|
1023
|
-
var
|
|
1022
|
+
run(t) {
|
|
1023
|
+
var e;
|
|
1024
1024
|
const a = [];
|
|
1025
|
-
for (const i of
|
|
1025
|
+
for (const i of t.querySelectorAll("img[alt]")) {
|
|
1026
1026
|
const n = i.getAttribute("alt").trim().toLowerCase();
|
|
1027
1027
|
if (!n) continue;
|
|
1028
1028
|
const r = i.closest("a, button");
|
|
1029
1029
|
if (r) {
|
|
1030
|
-
const o = ((
|
|
1030
|
+
const o = ((e = r.textContent) == null ? void 0 : e.trim().toLowerCase()) || "";
|
|
1031
1031
|
if (o && o === n) {
|
|
1032
1032
|
const s = r.tagName.toLowerCase(), l = r.getAttribute("href");
|
|
1033
1033
|
a.push({
|
|
@@ -1051,19 +1051,19 @@ const Xe = {
|
|
|
1051
1051
|
description: "Image alt text should not contain words like 'image', 'photo', or 'picture' — screen readers already announce the element type.",
|
|
1052
1052
|
guidance: "Screen readers already announce 'image' or 'graphic' before reading alt text, so phrases like 'image of', 'photo of', or 'picture of' are redundant. Remove these words and describe what the image shows. For example, change 'image of a dog' to 'golden retriever playing fetch'.",
|
|
1053
1053
|
prompt: "The alt text contains a word like 'image', 'photo', or 'picture' that is already announced by the screen reader. Rewrite the alt text with the redundant word removed while keeping the description meaningful. For example: 'image of a sunset over the ocean' → 'sunset over the ocean'; 'photo of team members' → 'team members at the 2024 offsite'; 'icon for settings' → 'settings'.",
|
|
1054
|
-
run(
|
|
1054
|
+
run(t) {
|
|
1055
1055
|
const a = [];
|
|
1056
|
-
for (const
|
|
1057
|
-
const i =
|
|
1056
|
+
for (const e of t.querySelectorAll("img[alt]")) {
|
|
1057
|
+
const i = e.getAttribute("alt").toLowerCase();
|
|
1058
1058
|
if (!i) continue;
|
|
1059
1059
|
const n = Qe.filter((r) => i.split(/\s+/).includes(r));
|
|
1060
1060
|
n.length > 0 && a.push({
|
|
1061
1061
|
ruleId: "image-alt-redundant-words",
|
|
1062
|
-
selector: m(
|
|
1063
|
-
html: d(
|
|
1062
|
+
selector: m(e),
|
|
1063
|
+
html: d(e),
|
|
1064
1064
|
impact: "minor",
|
|
1065
|
-
message: `Alt text "${
|
|
1066
|
-
context: `Current alt: "${
|
|
1065
|
+
message: `Alt text "${e.getAttribute("alt")}" contains redundant word(s): ${n.join(", ")}.`,
|
|
1066
|
+
context: `Current alt: "${e.getAttribute("alt")}", redundant word(s): ${n.join(", ")}`
|
|
1067
1067
|
});
|
|
1068
1068
|
}
|
|
1069
1069
|
return a;
|
|
@@ -1075,14 +1075,14 @@ const Xe = {
|
|
|
1075
1075
|
description: "Image map <area> elements must have alternative text.",
|
|
1076
1076
|
guidance: "Each clickable region in an image map needs alternative text so screen reader users know what the region represents. Add an alt attribute to every <area> element describing its purpose. For complex image maps, consider using alternative approaches like SVG with embedded links, or a list of text links.",
|
|
1077
1077
|
prompt: "Based on the href or shape/coords, suggest alt text describing where this area links or what it represents.",
|
|
1078
|
-
run(
|
|
1078
|
+
run(t) {
|
|
1079
1079
|
const a = [];
|
|
1080
|
-
for (const
|
|
1081
|
-
if (g(
|
|
1082
|
-
v(
|
|
1080
|
+
for (const e of t.querySelectorAll("area[href]")) {
|
|
1081
|
+
if (g(e)) continue;
|
|
1082
|
+
v(e) || a.push({
|
|
1083
1083
|
ruleId: "area-alt",
|
|
1084
|
-
selector: m(
|
|
1085
|
-
html: d(
|
|
1084
|
+
selector: m(e),
|
|
1085
|
+
html: d(e),
|
|
1086
1086
|
impact: "critical",
|
|
1087
1087
|
message: "Image map <area> element is missing alternative text."
|
|
1088
1088
|
});
|
|
@@ -1090,19 +1090,19 @@ const Xe = {
|
|
|
1090
1090
|
return a;
|
|
1091
1091
|
}
|
|
1092
1092
|
};
|
|
1093
|
-
function tt(
|
|
1093
|
+
function tt(t) {
|
|
1094
1094
|
var n, r;
|
|
1095
|
-
const a =
|
|
1095
|
+
const a = t.getAttribute("aria-labelledby");
|
|
1096
1096
|
if (a) {
|
|
1097
1097
|
const o = a.split(/\s+/).map((s) => {
|
|
1098
|
-
var l,
|
|
1099
|
-
return ((
|
|
1098
|
+
var l, h;
|
|
1099
|
+
return ((h = (l = t.ownerDocument.getElementById(s)) == null ? void 0 : l.textContent) == null ? void 0 : h.trim()) ?? "";
|
|
1100
1100
|
}).filter(Boolean);
|
|
1101
1101
|
if (o.length) return o.join(" ");
|
|
1102
1102
|
}
|
|
1103
|
-
const
|
|
1104
|
-
if (
|
|
1105
|
-
const i = (r =
|
|
1103
|
+
const e = (n = t.getAttribute("aria-label")) == null ? void 0 : n.trim();
|
|
1104
|
+
if (e) return e;
|
|
1105
|
+
const i = (r = t.getAttribute("title")) == null ? void 0 : r.trim();
|
|
1106
1106
|
return i || "";
|
|
1107
1107
|
}
|
|
1108
1108
|
const at = {
|
|
@@ -1112,10 +1112,10 @@ const at = {
|
|
|
1112
1112
|
description: "<object> elements must have alternative text.",
|
|
1113
1113
|
guidance: "Object elements embed external content that may not be accessible to all users. Provide alternative text via aria-label, aria-labelledby, or a title attribute. The fallback content inside <object> is only shown when the object fails to load and does not serve as an accessible name.",
|
|
1114
1114
|
prompt: "Based on the data/type attributes, suggest adding aria-label or a title attribute describing what the embedded content represents.",
|
|
1115
|
-
run(
|
|
1116
|
-
var
|
|
1115
|
+
run(t) {
|
|
1116
|
+
var e;
|
|
1117
1117
|
const a = [];
|
|
1118
|
-
for (const i of
|
|
1118
|
+
for (const i of t.querySelectorAll("object")) {
|
|
1119
1119
|
if (g(i) || i instanceof HTMLElement && i.style.visibility === "hidden") continue;
|
|
1120
1120
|
let n = i.parentElement, r = !1;
|
|
1121
1121
|
for (; n; ) {
|
|
@@ -1128,8 +1128,8 @@ const at = {
|
|
|
1128
1128
|
if (r || i.getAttribute("role") === "presentation" || i.getAttribute("role") === "none" || tt(i)) continue;
|
|
1129
1129
|
const o = i.getAttribute("data") || "";
|
|
1130
1130
|
if (!((i.getAttribute("type") || "").startsWith("image/") || /\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i.test(o))) {
|
|
1131
|
-
const
|
|
1132
|
-
if (
|
|
1131
|
+
const h = i.querySelector("img[alt]");
|
|
1132
|
+
if (h && ((e = h.getAttribute("alt")) != null && e.trim())) continue;
|
|
1133
1133
|
}
|
|
1134
1134
|
a.push({
|
|
1135
1135
|
ruleId: "object-alt",
|
|
@@ -1148,14 +1148,14 @@ const at = {
|
|
|
1148
1148
|
description: "Elements with role='img' must have an accessible name.",
|
|
1149
1149
|
guidance: "When you assign role='img' to an element (like a div containing icon fonts or CSS backgrounds), you must provide an accessible name via aria-label or aria-labelledby. Without this, screen reader users have no way to understand what the image represents. If the image is decorative, use role='presentation' or role='none' instead.",
|
|
1150
1150
|
prompt: "Based on the element's content or class names, suggest either an aria-label describing the image, or if decorative, recommend removing role='img' or adding aria-hidden='true'.",
|
|
1151
|
-
run(
|
|
1151
|
+
run(t) {
|
|
1152
1152
|
const a = [];
|
|
1153
|
-
for (const
|
|
1154
|
-
if (g(
|
|
1155
|
-
v(
|
|
1153
|
+
for (const e of t.querySelectorAll('[role="img"]')) {
|
|
1154
|
+
if (g(e) || e.tagName.toLowerCase() === "svg" || e.tagName.toLowerCase() === "img") continue;
|
|
1155
|
+
v(e) || a.push({
|
|
1156
1156
|
ruleId: "role-img-alt",
|
|
1157
|
-
selector: m(
|
|
1158
|
-
html: d(
|
|
1157
|
+
selector: m(e),
|
|
1158
|
+
html: d(e),
|
|
1159
1159
|
impact: "serious",
|
|
1160
1160
|
message: "Element with role='img' has no accessible name. Add aria-label or aria-labelledby."
|
|
1161
1161
|
});
|
|
@@ -1163,17 +1163,17 @@ const at = {
|
|
|
1163
1163
|
return a;
|
|
1164
1164
|
}
|
|
1165
1165
|
};
|
|
1166
|
-
function Ni(
|
|
1167
|
-
if (typeof
|
|
1166
|
+
function Ni(t) {
|
|
1167
|
+
if (typeof t != "object" || t === null)
|
|
1168
1168
|
return "Rule spec must be an object";
|
|
1169
|
-
const a =
|
|
1169
|
+
const a = t;
|
|
1170
1170
|
if (typeof a.id != "string" || a.id.length === 0)
|
|
1171
1171
|
return "Rule must have a non-empty string id";
|
|
1172
1172
|
if (typeof a.selector != "string" || a.selector.length === 0)
|
|
1173
1173
|
return "Rule must have a non-empty string selector";
|
|
1174
1174
|
if (typeof a.check != "object" || a.check === null)
|
|
1175
1175
|
return "Rule must have a check object";
|
|
1176
|
-
const
|
|
1176
|
+
const e = a.check;
|
|
1177
1177
|
if (![
|
|
1178
1178
|
"selector-exists",
|
|
1179
1179
|
"attribute-value",
|
|
@@ -1181,8 +1181,8 @@ function Ni(e) {
|
|
|
1181
1181
|
"attribute-regex",
|
|
1182
1182
|
"child-required",
|
|
1183
1183
|
"child-invalid"
|
|
1184
|
-
].includes(
|
|
1185
|
-
return `Invalid check type: ${String(
|
|
1184
|
+
].includes(e.type))
|
|
1185
|
+
return `Invalid check type: ${String(e.type)}`;
|
|
1186
1186
|
if (typeof a.impact != "string" || !["critical", "serious", "moderate", "minor"].includes(a.impact))
|
|
1187
1187
|
return "Rule must have a valid impact (critical|serious|moderate|minor)";
|
|
1188
1188
|
if (typeof a.message != "string" || a.message.length === 0)
|
|
@@ -1193,171 +1193,171 @@ function Ni(e) {
|
|
|
1193
1193
|
return "Rule must have a wcag array";
|
|
1194
1194
|
if (typeof a.level != "string" || !["A", "AA"].includes(a.level))
|
|
1195
1195
|
return "Rule must have level A or AA";
|
|
1196
|
-
const n = nt(
|
|
1196
|
+
const n = nt(e);
|
|
1197
1197
|
return n || null;
|
|
1198
1198
|
}
|
|
1199
|
-
function nt(
|
|
1200
|
-
switch (
|
|
1199
|
+
function nt(t) {
|
|
1200
|
+
switch (t.type) {
|
|
1201
1201
|
case "selector-exists":
|
|
1202
1202
|
return null;
|
|
1203
1203
|
case "attribute-value":
|
|
1204
|
-
return typeof
|
|
1204
|
+
return typeof t.attribute != "string" ? "attribute-value check requires attribute string" : [">", "<", "=", "!=", "in", "not-in"].includes(t.operator) ? t.value === void 0 ? "attribute-value check requires value" : null : "attribute-value check requires valid operator";
|
|
1205
1205
|
case "attribute-missing":
|
|
1206
|
-
return typeof
|
|
1206
|
+
return typeof t.attribute != "string" ? "attribute-missing check requires attribute string" : null;
|
|
1207
1207
|
case "attribute-regex":
|
|
1208
|
-
return typeof
|
|
1208
|
+
return typeof t.attribute != "string" ? "attribute-regex check requires attribute string" : typeof t.pattern != "string" ? "attribute-regex check requires pattern string" : typeof t.shouldMatch != "boolean" ? "attribute-regex check requires shouldMatch boolean" : null;
|
|
1209
1209
|
case "child-required":
|
|
1210
|
-
return typeof
|
|
1210
|
+
return typeof t.childSelector != "string" ? "child-required check requires childSelector string" : null;
|
|
1211
1211
|
case "child-invalid":
|
|
1212
|
-
return Array.isArray(
|
|
1212
|
+
return Array.isArray(t.allowedChildren) ? null : "child-invalid check requires allowedChildren array";
|
|
1213
1213
|
default:
|
|
1214
|
-
return `Unknown check type: ${String(
|
|
1214
|
+
return `Unknown check type: ${String(t.type)}`;
|
|
1215
1215
|
}
|
|
1216
1216
|
}
|
|
1217
|
-
function
|
|
1218
|
-
let i =
|
|
1217
|
+
function k(t, a, e) {
|
|
1218
|
+
let i = t;
|
|
1219
1219
|
if (i.includes("{{tag}}") && (i = i.replace(/\{\{tag\}\}/g, a.tagName.toLowerCase())), i.includes("{{value}}")) {
|
|
1220
1220
|
let n = "";
|
|
1221
|
-
"attribute" in
|
|
1221
|
+
"attribute" in e && e.attribute && (n = a.getAttribute(e.attribute) ?? ""), i = i.replace(/\{\{value\}\}/g, n);
|
|
1222
1222
|
}
|
|
1223
1223
|
return i;
|
|
1224
1224
|
}
|
|
1225
|
-
function
|
|
1226
|
-
const a =
|
|
1225
|
+
function I(t) {
|
|
1226
|
+
const a = t.skipAriaHidden !== !1;
|
|
1227
1227
|
return {
|
|
1228
|
-
id:
|
|
1229
|
-
wcag:
|
|
1230
|
-
level:
|
|
1231
|
-
tags:
|
|
1232
|
-
description:
|
|
1233
|
-
guidance:
|
|
1234
|
-
prompt:
|
|
1235
|
-
run(
|
|
1228
|
+
id: t.id,
|
|
1229
|
+
wcag: t.wcag,
|
|
1230
|
+
level: t.level,
|
|
1231
|
+
tags: t.tags,
|
|
1232
|
+
description: t.description,
|
|
1233
|
+
guidance: t.guidance,
|
|
1234
|
+
prompt: t.prompt,
|
|
1235
|
+
run(e) {
|
|
1236
1236
|
var n, r;
|
|
1237
1237
|
const i = [];
|
|
1238
|
-
switch (
|
|
1238
|
+
switch (t.check.type) {
|
|
1239
1239
|
case "selector-exists": {
|
|
1240
|
-
for (const o of
|
|
1240
|
+
for (const o of e.querySelectorAll(t.selector))
|
|
1241
1241
|
a && g(o) || i.push({
|
|
1242
|
-
ruleId:
|
|
1242
|
+
ruleId: t.id,
|
|
1243
1243
|
selector: m(o),
|
|
1244
1244
|
html: d(o),
|
|
1245
|
-
impact:
|
|
1246
|
-
message:
|
|
1245
|
+
impact: t.impact,
|
|
1246
|
+
message: k(t.message, o, t.check),
|
|
1247
1247
|
element: o
|
|
1248
1248
|
});
|
|
1249
1249
|
break;
|
|
1250
1250
|
}
|
|
1251
1251
|
case "attribute-value": {
|
|
1252
|
-
const { attribute: o, operator: s, value: l } =
|
|
1253
|
-
for (const
|
|
1254
|
-
if (a && g(
|
|
1255
|
-
const c =
|
|
1252
|
+
const { attribute: o, operator: s, value: l } = t.check;
|
|
1253
|
+
for (const h of e.querySelectorAll(t.selector)) {
|
|
1254
|
+
if (a && g(h)) continue;
|
|
1255
|
+
const c = h.getAttribute(o);
|
|
1256
1256
|
c !== null && rt(c, s, l) && i.push({
|
|
1257
|
-
ruleId:
|
|
1258
|
-
selector: m(
|
|
1259
|
-
html: d(
|
|
1260
|
-
impact:
|
|
1261
|
-
message:
|
|
1262
|
-
element:
|
|
1257
|
+
ruleId: t.id,
|
|
1258
|
+
selector: m(h),
|
|
1259
|
+
html: d(h),
|
|
1260
|
+
impact: t.impact,
|
|
1261
|
+
message: k(t.message, h, t.check),
|
|
1262
|
+
element: h
|
|
1263
1263
|
});
|
|
1264
1264
|
}
|
|
1265
1265
|
break;
|
|
1266
1266
|
}
|
|
1267
1267
|
case "attribute-missing": {
|
|
1268
|
-
const { attribute: o } =
|
|
1269
|
-
for (const s of
|
|
1268
|
+
const { attribute: o } = t.check;
|
|
1269
|
+
for (const s of e.querySelectorAll(t.selector))
|
|
1270
1270
|
a && g(s) || s.hasAttribute(o) || i.push({
|
|
1271
|
-
ruleId:
|
|
1271
|
+
ruleId: t.id,
|
|
1272
1272
|
selector: m(s),
|
|
1273
1273
|
html: d(s),
|
|
1274
|
-
impact:
|
|
1275
|
-
message:
|
|
1274
|
+
impact: t.impact,
|
|
1275
|
+
message: k(t.message, s, t.check),
|
|
1276
1276
|
element: s
|
|
1277
1277
|
});
|
|
1278
1278
|
break;
|
|
1279
1279
|
}
|
|
1280
1280
|
case "attribute-regex": {
|
|
1281
|
-
const { attribute: o, pattern: s, flags: l, shouldMatch:
|
|
1281
|
+
const { attribute: o, pattern: s, flags: l, shouldMatch: h } = t.check;
|
|
1282
1282
|
let c;
|
|
1283
1283
|
try {
|
|
1284
1284
|
c = new RegExp(s, l);
|
|
1285
1285
|
} catch {
|
|
1286
1286
|
break;
|
|
1287
1287
|
}
|
|
1288
|
-
for (const u of
|
|
1288
|
+
for (const u of e.querySelectorAll(t.selector)) {
|
|
1289
1289
|
if (a && g(u)) continue;
|
|
1290
|
-
const
|
|
1291
|
-
if (
|
|
1292
|
-
const b = c.test(
|
|
1293
|
-
|
|
1294
|
-
ruleId:
|
|
1290
|
+
const p = u.getAttribute(o);
|
|
1291
|
+
if (p === null) continue;
|
|
1292
|
+
const b = c.test(p);
|
|
1293
|
+
h && !b ? i.push({
|
|
1294
|
+
ruleId: t.id,
|
|
1295
1295
|
selector: m(u),
|
|
1296
1296
|
html: d(u),
|
|
1297
|
-
impact:
|
|
1298
|
-
message:
|
|
1297
|
+
impact: t.impact,
|
|
1298
|
+
message: k(t.message, u, t.check),
|
|
1299
1299
|
element: u
|
|
1300
|
-
}) : !
|
|
1301
|
-
ruleId:
|
|
1300
|
+
}) : !h && b && i.push({
|
|
1301
|
+
ruleId: t.id,
|
|
1302
1302
|
selector: m(u),
|
|
1303
1303
|
html: d(u),
|
|
1304
|
-
impact:
|
|
1305
|
-
message:
|
|
1304
|
+
impact: t.impact,
|
|
1305
|
+
message: k(t.message, u, t.check),
|
|
1306
1306
|
element: u
|
|
1307
1307
|
});
|
|
1308
1308
|
}
|
|
1309
1309
|
break;
|
|
1310
1310
|
}
|
|
1311
1311
|
case "child-required": {
|
|
1312
|
-
const { childSelector: o } =
|
|
1313
|
-
for (const s of
|
|
1312
|
+
const { childSelector: o } = t.check;
|
|
1313
|
+
for (const s of e.querySelectorAll(t.selector))
|
|
1314
1314
|
a && g(s) || s.querySelector(o) || i.push({
|
|
1315
|
-
ruleId:
|
|
1315
|
+
ruleId: t.id,
|
|
1316
1316
|
selector: m(s),
|
|
1317
1317
|
html: d(s),
|
|
1318
|
-
impact:
|
|
1319
|
-
message:
|
|
1318
|
+
impact: t.impact,
|
|
1319
|
+
message: k(t.message, s, t.check),
|
|
1320
1320
|
element: s
|
|
1321
1321
|
});
|
|
1322
1322
|
break;
|
|
1323
1323
|
}
|
|
1324
1324
|
case "child-invalid": {
|
|
1325
1325
|
const o = new Set(
|
|
1326
|
-
|
|
1327
|
-
), s =
|
|
1328
|
-
for (const l of
|
|
1326
|
+
t.check.allowedChildren.map((l) => l.toLowerCase())
|
|
1327
|
+
), s = t.check.allowedChildRoles ? new Set(t.check.allowedChildRoles.map((l) => l.toLowerCase())) : null;
|
|
1328
|
+
for (const l of e.querySelectorAll(t.selector)) {
|
|
1329
1329
|
if (a && g(l)) continue;
|
|
1330
|
-
const
|
|
1331
|
-
if (
|
|
1330
|
+
const h = (n = l.getAttribute("role")) == null ? void 0 : n.trim().toLowerCase();
|
|
1331
|
+
if (h === "presentation" || h === "none") continue;
|
|
1332
1332
|
let c = !1;
|
|
1333
|
-
const u =
|
|
1334
|
-
(
|
|
1333
|
+
const u = t.check.allowedChildren.filter(
|
|
1334
|
+
(p) => p !== "script" && p !== "template"
|
|
1335
1335
|
);
|
|
1336
|
-
for (const
|
|
1337
|
-
if (
|
|
1336
|
+
for (const p of l.childNodes)
|
|
1337
|
+
if (p.nodeType === 3 && p.textContent && p.textContent.trim()) {
|
|
1338
1338
|
const b = u.map((f) => `<${f}>`).join(" or ");
|
|
1339
1339
|
i.push({
|
|
1340
|
-
ruleId:
|
|
1340
|
+
ruleId: t.id,
|
|
1341
1341
|
selector: m(l),
|
|
1342
1342
|
html: d(l),
|
|
1343
|
-
impact:
|
|
1343
|
+
impact: t.impact,
|
|
1344
1344
|
message: `<${l.tagName.toLowerCase()}> contains direct text content. Wrap in ${b}.`,
|
|
1345
1345
|
element: l
|
|
1346
1346
|
}), c = !0;
|
|
1347
1347
|
break;
|
|
1348
1348
|
}
|
|
1349
1349
|
if (!c)
|
|
1350
|
-
for (const
|
|
1351
|
-
if (o.has(
|
|
1352
|
-
const b = (r =
|
|
1350
|
+
for (const p of l.children) {
|
|
1351
|
+
if (o.has(p.tagName.toLowerCase())) continue;
|
|
1352
|
+
const b = (r = p.getAttribute("role")) == null ? void 0 : r.trim().toLowerCase();
|
|
1353
1353
|
if (!(b && (s != null && s.has(b))) && !(b === "presentation" || b === "none")) {
|
|
1354
1354
|
i.push({
|
|
1355
|
-
ruleId:
|
|
1356
|
-
selector: m(
|
|
1357
|
-
html: d(
|
|
1358
|
-
impact:
|
|
1359
|
-
message:
|
|
1360
|
-
element:
|
|
1355
|
+
ruleId: t.id,
|
|
1356
|
+
selector: m(p),
|
|
1357
|
+
html: d(p),
|
|
1358
|
+
impact: t.impact,
|
|
1359
|
+
message: k(t.message, p, t.check),
|
|
1360
|
+
element: p
|
|
1361
1361
|
});
|
|
1362
1362
|
break;
|
|
1363
1363
|
}
|
|
@@ -1370,20 +1370,20 @@ function k(e) {
|
|
|
1370
1370
|
}
|
|
1371
1371
|
};
|
|
1372
1372
|
}
|
|
1373
|
-
function rt(
|
|
1373
|
+
function rt(t, a, e) {
|
|
1374
1374
|
switch (a) {
|
|
1375
1375
|
case ">":
|
|
1376
|
-
return parseFloat(
|
|
1376
|
+
return parseFloat(t) > e;
|
|
1377
1377
|
case "<":
|
|
1378
|
-
return parseFloat(
|
|
1378
|
+
return parseFloat(t) < e;
|
|
1379
1379
|
case "=":
|
|
1380
|
-
return
|
|
1380
|
+
return t === String(e);
|
|
1381
1381
|
case "!=":
|
|
1382
|
-
return
|
|
1382
|
+
return t !== String(e);
|
|
1383
1383
|
case "in":
|
|
1384
|
-
return Array.isArray(
|
|
1384
|
+
return Array.isArray(e) && e.includes(t);
|
|
1385
1385
|
case "not-in":
|
|
1386
|
-
return Array.isArray(
|
|
1386
|
+
return Array.isArray(e) && !e.includes(t);
|
|
1387
1387
|
default:
|
|
1388
1388
|
return !1;
|
|
1389
1389
|
}
|
|
@@ -1399,7 +1399,7 @@ const ot = {
|
|
|
1399
1399
|
level: "A",
|
|
1400
1400
|
guidance: "Server-side image maps (using ismap attribute) send click coordinates to the server, which is inaccessible to keyboard users and screen readers who can't precisely click specific regions. Replace with client-side image maps (<map> with <area> elements) that provide keyboard access and accessible names, or use linked images/buttons instead.",
|
|
1401
1401
|
prompt: "Explain that the ismap attribute should be removed and the functionality replaced with a client-side <map> element with <area> children, or separate linked images/buttons."
|
|
1402
|
-
}, st =
|
|
1402
|
+
}, st = I(ot), lt = [
|
|
1403
1403
|
'[role="checkbox"]',
|
|
1404
1404
|
'[role="combobox"]',
|
|
1405
1405
|
'[role="listbox"]',
|
|
@@ -1425,39 +1425,39 @@ const ot = {
|
|
|
1425
1425
|
"spinbutton",
|
|
1426
1426
|
"textbox"
|
|
1427
1427
|
]);
|
|
1428
|
-
function dt(
|
|
1429
|
-
var o, s, l,
|
|
1430
|
-
const a = (o =
|
|
1431
|
-
if (a && ct.has(a) || (
|
|
1432
|
-
return v(
|
|
1433
|
-
const i =
|
|
1428
|
+
function dt(t) {
|
|
1429
|
+
var o, s, l, h;
|
|
1430
|
+
const a = (o = t.getAttribute("role")) == null ? void 0 : o.trim().toLowerCase();
|
|
1431
|
+
if (a && ct.has(a) || (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) && !(a && ut.has(a)))
|
|
1432
|
+
return v(t);
|
|
1433
|
+
const i = t.getAttribute("aria-labelledby");
|
|
1434
1434
|
if (i) {
|
|
1435
1435
|
const c = i.split(/\s+/).map((u) => {
|
|
1436
|
-
const
|
|
1437
|
-
return
|
|
1436
|
+
const p = t.ownerDocument.getElementById(u);
|
|
1437
|
+
return p ? A(p).trim() : "";
|
|
1438
1438
|
}).filter(Boolean);
|
|
1439
1439
|
if (c.length) return c.join(" ");
|
|
1440
1440
|
}
|
|
1441
|
-
const n = (s =
|
|
1441
|
+
const n = (s = t.getAttribute("aria-label")) == null ? void 0 : s.trim();
|
|
1442
1442
|
if (n) return n;
|
|
1443
|
-
if (
|
|
1444
|
-
if (
|
|
1445
|
-
const u =
|
|
1443
|
+
if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) {
|
|
1444
|
+
if (t.id) {
|
|
1445
|
+
const u = t.ownerDocument.querySelector(`label[for="${CSS.escape(t.id)}"]`);
|
|
1446
1446
|
if (u) {
|
|
1447
|
-
const
|
|
1448
|
-
if (
|
|
1447
|
+
const p = A(u).trim();
|
|
1448
|
+
if (p) return p;
|
|
1449
1449
|
}
|
|
1450
1450
|
}
|
|
1451
|
-
const c =
|
|
1451
|
+
const c = t.closest("label");
|
|
1452
1452
|
if (c) {
|
|
1453
1453
|
const u = A(c).trim();
|
|
1454
1454
|
if (u) return u;
|
|
1455
1455
|
}
|
|
1456
1456
|
}
|
|
1457
|
-
const r = (l =
|
|
1457
|
+
const r = (l = t.getAttribute("title")) == null ? void 0 : l.trim();
|
|
1458
1458
|
if (r) return r;
|
|
1459
|
-
if (
|
|
1460
|
-
const c = (
|
|
1459
|
+
if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) {
|
|
1460
|
+
const c = (h = t.getAttribute("placeholder")) == null ? void 0 : h.trim();
|
|
1461
1461
|
if (c) return c;
|
|
1462
1462
|
}
|
|
1463
1463
|
return "";
|
|
@@ -1469,20 +1469,20 @@ const mt = {
|
|
|
1469
1469
|
description: "Form elements must have labels. Use <label>, aria-label, or aria-labelledby.",
|
|
1470
1470
|
guidance: "Every form input needs an accessible label so users understand what information to enter. Use a <label> element with a for attribute matching the input's id, wrap the input in a <label>, or use aria-label/aria-labelledby for custom components. Placeholders are not sufficient as labels since they disappear when typing.",
|
|
1471
1471
|
prompt: `This form field has no accessible label. Based on the context (input type, name attribute, placeholder, or surrounding elements), suggest adding a <label for="id"> element with descriptive text, or an aria-label attribute. The label should describe what information the user should enter, not the field type. For example: 'Email address', 'Search', 'Phone number'.`,
|
|
1472
|
-
run(
|
|
1472
|
+
run(t) {
|
|
1473
1473
|
var n;
|
|
1474
|
-
const a = [], i =
|
|
1474
|
+
const a = [], i = t.querySelectorAll(`input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]), textarea, select, ${lt}`);
|
|
1475
1475
|
for (const r of i) {
|
|
1476
1476
|
if (g(r) || T(r) || (r instanceof HTMLInputElement || r instanceof HTMLTextAreaElement || r instanceof HTMLSelectElement || r instanceof HTMLButtonElement) && r.disabled || r.closest("fieldset[disabled]") || r.getAttribute("aria-disabled") === "true") continue;
|
|
1477
1477
|
const o = (n = r.getAttribute("role")) == null ? void 0 : n.trim().toLowerCase();
|
|
1478
1478
|
if (o === "presentation" || o === "none") continue;
|
|
1479
1479
|
if (!dt(r)) {
|
|
1480
|
-
const l = [],
|
|
1481
|
-
c &&
|
|
1480
|
+
const l = [], h = r.tagName.toLowerCase(), c = r.getAttribute("type");
|
|
1481
|
+
c && h === "input" && l.push(`type: ${c}`);
|
|
1482
1482
|
const u = r.getAttribute("name");
|
|
1483
1483
|
u && l.push(`name: "${u}"`);
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1484
|
+
const p = r.getAttribute("placeholder");
|
|
1485
|
+
p && l.push(`placeholder: "${p}"`), o && l.push(`role: ${o}`);
|
|
1486
1486
|
const b = r.getAttribute("id");
|
|
1487
1487
|
b && l.push(`id: "${b}"`), a.push({
|
|
1488
1488
|
ruleId: "label",
|
|
@@ -1504,11 +1504,11 @@ const mt = {
|
|
|
1504
1504
|
description: "Form fields should not have multiple label elements.",
|
|
1505
1505
|
guidance: "When a form field has multiple <label> elements pointing to it, assistive technologies may announce only one label or behave inconsistently. Use a single <label> and combine any additional text into it, or use aria-describedby for supplementary information.",
|
|
1506
1506
|
prompt: "Identify the multiple labels and recommend consolidating them into a single <label> element or using aria-describedby for supplementary text.",
|
|
1507
|
-
run(
|
|
1508
|
-
const a = [],
|
|
1509
|
-
for (const i of
|
|
1507
|
+
run(t) {
|
|
1508
|
+
const a = [], e = t.querySelectorAll('input:not([type="hidden"]), textarea, select');
|
|
1509
|
+
for (const i of e) {
|
|
1510
1510
|
if (g(i) || !i.id) continue;
|
|
1511
|
-
const n =
|
|
1511
|
+
const n = t.querySelectorAll(`label[for="${CSS.escape(i.id)}"]`);
|
|
1512
1512
|
let r = 0, o = i.parentElement;
|
|
1513
1513
|
for (; o; ) {
|
|
1514
1514
|
if (o.tagName.toLowerCase() === "label" && !o.hasAttribute("for")) {
|
|
@@ -1535,13 +1535,13 @@ const mt = {
|
|
|
1535
1535
|
description: "Select elements must have a programmatically associated label via <label>, aria-label, or aria-labelledby.",
|
|
1536
1536
|
guidance: "Select dropdowns need labels so users understand what choice they're making. Use a <label> element with a for attribute matching the select's id, or wrap the select in a <label>. For selects without visible labels, use aria-label. The first <option> is not a substitute for a proper label.",
|
|
1537
1537
|
prompt: "Based on the options or context, suggest a label element or aria-label describing what this select controls.",
|
|
1538
|
-
run(
|
|
1538
|
+
run(t) {
|
|
1539
1539
|
const a = [];
|
|
1540
|
-
for (const
|
|
1541
|
-
g(
|
|
1540
|
+
for (const e of t.querySelectorAll("select"))
|
|
1541
|
+
g(e) || v(e) || a.push({
|
|
1542
1542
|
ruleId: "select-name",
|
|
1543
|
-
selector: m(
|
|
1544
|
-
html: d(
|
|
1543
|
+
selector: m(e),
|
|
1544
|
+
html: d(e),
|
|
1545
1545
|
impact: "critical",
|
|
1546
1546
|
message: "Select element has no accessible name."
|
|
1547
1547
|
});
|
|
@@ -1554,14 +1554,14 @@ const mt = {
|
|
|
1554
1554
|
description: "Input buttons must have discernible text via value, aria-label, or aria-labelledby.",
|
|
1555
1555
|
guidance: "Input buttons (<input type='submit'>, type='button', type='reset'>) need accessible names so users know what action the button performs. Add a value attribute with descriptive text (e.g., value='Submit Form'), or use aria-label if the value must differ from the accessible name.",
|
|
1556
1556
|
prompt: "Based on the input type and form context, suggest a value attribute describing the button's action.",
|
|
1557
|
-
run(
|
|
1558
|
-
var
|
|
1557
|
+
run(t) {
|
|
1558
|
+
var e, i;
|
|
1559
1559
|
const a = [];
|
|
1560
|
-
for (const n of
|
|
1560
|
+
for (const n of t.querySelectorAll(
|
|
1561
1561
|
'input[type="submit"], input[type="button"], input[type="reset"]'
|
|
1562
1562
|
)) {
|
|
1563
1563
|
if (g(n)) continue;
|
|
1564
|
-
const r = (
|
|
1564
|
+
const r = (e = n.getAttribute("value")) == null ? void 0 : e.trim(), o = (i = n.getAttribute("type")) == null ? void 0 : i.toLowerCase(), s = (o === "submit" || o === "reset") && !n.hasAttribute("value");
|
|
1565
1565
|
!r && !s && !v(n) && a.push({
|
|
1566
1566
|
ruleId: "input-button-name",
|
|
1567
1567
|
selector: m(n),
|
|
@@ -1636,16 +1636,16 @@ const mt = {
|
|
|
1636
1636
|
"tel-extension",
|
|
1637
1637
|
"email",
|
|
1638
1638
|
"impp"
|
|
1639
|
-
]), vt = /* @__PURE__ */ new Set(["home", "work", "mobile", "fax", "pager"]),
|
|
1640
|
-
function At(
|
|
1641
|
-
const a =
|
|
1639
|
+
]), vt = /* @__PURE__ */ new Set(["home", "work", "mobile", "fax", "pager"]), yt = /* @__PURE__ */ new Set(["shipping", "billing"]), wt = /* @__PURE__ */ new Set(["webauthn"]);
|
|
1640
|
+
function At(t) {
|
|
1641
|
+
const a = t.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1642
1642
|
if (a.length === 0) return !0;
|
|
1643
|
-
let
|
|
1644
|
-
a[
|
|
1643
|
+
let e = 0;
|
|
1644
|
+
a[e].startsWith("section-") && e++, e < a.length && yt.has(a[e]) && e++;
|
|
1645
1645
|
let i = !1;
|
|
1646
|
-
if (
|
|
1647
|
-
const n = a[
|
|
1648
|
-
return !bt.has(n) || i && !ft.has(n) ? !1 : (
|
|
1646
|
+
if (e < a.length && vt.has(a[e]) && (i = !0, e++), e >= a.length) return !1;
|
|
1647
|
+
const n = a[e];
|
|
1648
|
+
return !bt.has(n) || i && !ft.has(n) ? !1 : (e++, e < a.length && wt.has(a[e]) && e++, e === a.length);
|
|
1649
1649
|
}
|
|
1650
1650
|
const xt = {
|
|
1651
1651
|
id: "autocomplete-valid",
|
|
@@ -1654,15 +1654,15 @@ const xt = {
|
|
|
1654
1654
|
description: "Autocomplete attribute must use valid values from the HTML specification.",
|
|
1655
1655
|
guidance: "The autocomplete attribute helps users fill forms by identifying input purposes. Use standard values like 'name', 'email', 'tel', 'street-address', 'postal-code', 'cc-number'. This benefits users with cognitive disabilities, motor impairments, and anyone using password managers or autofill. Check the HTML specification for the complete list of valid tokens.",
|
|
1656
1656
|
prompt: "Show the invalid autocomplete value and suggest the correct standard value based on the input's apparent purpose.",
|
|
1657
|
-
run(
|
|
1657
|
+
run(t) {
|
|
1658
1658
|
const a = [];
|
|
1659
|
-
for (const
|
|
1660
|
-
if (g(
|
|
1661
|
-
const i =
|
|
1659
|
+
for (const e of t.querySelectorAll("[autocomplete]")) {
|
|
1660
|
+
if (g(e) || e instanceof HTMLElement && e.style.display === "none" || e.disabled || e.getAttribute("aria-disabled") === "true") continue;
|
|
1661
|
+
const i = e.getAttribute("autocomplete").trim();
|
|
1662
1662
|
i && (At(i) || a.push({
|
|
1663
1663
|
ruleId: "autocomplete-valid",
|
|
1664
|
-
selector: m(
|
|
1665
|
-
html: d(
|
|
1664
|
+
selector: m(e),
|
|
1665
|
+
html: d(e),
|
|
1666
1666
|
impact: "serious",
|
|
1667
1667
|
message: `Invalid autocomplete value "${i}".`
|
|
1668
1668
|
}));
|
|
@@ -1670,26 +1670,26 @@ const xt = {
|
|
|
1670
1670
|
return a;
|
|
1671
1671
|
}
|
|
1672
1672
|
};
|
|
1673
|
-
function
|
|
1674
|
-
return
|
|
1673
|
+
function te(t) {
|
|
1674
|
+
return t.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1675
1675
|
}
|
|
1676
|
-
function
|
|
1677
|
-
const
|
|
1678
|
-
if (!
|
|
1676
|
+
function ae(t, a) {
|
|
1677
|
+
const e = te(t), i = te(a);
|
|
1678
|
+
if (!e || !i || e.includes(i) || i.includes(e)) return !0;
|
|
1679
1679
|
const n = i.split(/\s+/).map((r) => r.replace(/[.,;:!?\u2026]+$/g, "")).filter((r) => r.length > 2);
|
|
1680
|
-
return n.length >= 2 && n.filter((o) =>
|
|
1680
|
+
return n.length >= 2 && n.filter((o) => e.includes(o)).length / n.length > 0.5;
|
|
1681
1681
|
}
|
|
1682
|
-
function
|
|
1682
|
+
function U(t) {
|
|
1683
1683
|
let a = "";
|
|
1684
|
-
for (const
|
|
1685
|
-
if (
|
|
1686
|
-
a +=
|
|
1687
|
-
else if (
|
|
1688
|
-
const i =
|
|
1684
|
+
for (const e of t.childNodes)
|
|
1685
|
+
if (e.nodeType === 3)
|
|
1686
|
+
a += e.textContent ?? "";
|
|
1687
|
+
else if (e.nodeType === 1) {
|
|
1688
|
+
const i = e, n = i.tagName.toLowerCase();
|
|
1689
1689
|
if (n === "style" || n === "script" || n === "svg" || i.getAttribute("aria-hidden") === "true" || i instanceof HTMLElement && i.style.display === "none") continue;
|
|
1690
1690
|
const r = i.getAttribute("role");
|
|
1691
1691
|
if (r === "img" || r === "presentation" || r === "none") continue;
|
|
1692
|
-
a +=
|
|
1692
|
+
a += U(i);
|
|
1693
1693
|
}
|
|
1694
1694
|
return a;
|
|
1695
1695
|
}
|
|
@@ -1701,39 +1701,39 @@ const St = {
|
|
|
1701
1701
|
description: "Interactive elements with visible text must have accessible names that contain that text.",
|
|
1702
1702
|
guidance: "For voice control users who activate controls by speaking their visible label, the accessible name must include the visible text. If aria-label is 'Submit form' but the button shows 'Send', voice users saying 'click Send' won't activate it. Ensure aria-label/aria-labelledby contains or matches the visible text.",
|
|
1703
1703
|
prompt: "Show the mismatch between the visible text and accessible name, and suggest updating aria-label to include the visible text.",
|
|
1704
|
-
run(
|
|
1704
|
+
run(t) {
|
|
1705
1705
|
const a = [];
|
|
1706
|
-
for (const
|
|
1707
|
-
if (g(
|
|
1708
|
-
const i = v(
|
|
1706
|
+
for (const e of t.querySelectorAll('button, [role="button"], a[href], input[type="submit"], input[type="button"]')) {
|
|
1707
|
+
if (g(e)) continue;
|
|
1708
|
+
const i = v(e);
|
|
1709
1709
|
if (!i) continue;
|
|
1710
1710
|
let n = "";
|
|
1711
|
-
|
|
1711
|
+
e instanceof HTMLInputElement ? n = e.value || "" : n = U(e);
|
|
1712
1712
|
const r = n.trim();
|
|
1713
1713
|
if (!r || r.length <= 2) continue;
|
|
1714
|
-
const o =
|
|
1715
|
-
!o && !s ||
|
|
1714
|
+
const o = e.hasAttribute("aria-label"), s = e.hasAttribute("aria-labelledby");
|
|
1715
|
+
!o && !s || ae(i, n) || a.push({
|
|
1716
1716
|
ruleId: "label-content-name-mismatch",
|
|
1717
|
-
selector: m(
|
|
1718
|
-
html: d(
|
|
1717
|
+
selector: m(e),
|
|
1718
|
+
html: d(e),
|
|
1719
1719
|
impact: "serious",
|
|
1720
1720
|
message: `Accessible name "${i}" does not contain visible text "${n.trim()}".`
|
|
1721
1721
|
});
|
|
1722
1722
|
}
|
|
1723
|
-
for (const
|
|
1724
|
-
if (g(
|
|
1725
|
-
const i = v(
|
|
1726
|
-
if (!i || !
|
|
1727
|
-
const r =
|
|
1723
|
+
for (const e of t.querySelectorAll("input, select, textarea")) {
|
|
1724
|
+
if (g(e) || e instanceof HTMLInputElement && ["hidden", "submit", "button", "image"].includes(e.type)) continue;
|
|
1725
|
+
const i = v(e);
|
|
1726
|
+
if (!i || !e.hasAttribute("aria-label")) continue;
|
|
1727
|
+
const r = e.id;
|
|
1728
1728
|
let o = "";
|
|
1729
1729
|
if (r) {
|
|
1730
|
-
const s =
|
|
1731
|
-
s && (o =
|
|
1730
|
+
const s = t.querySelector(`label[for="${CSS.escape(r)}"]`);
|
|
1731
|
+
s && (o = U(s));
|
|
1732
1732
|
}
|
|
1733
|
-
o.trim() && (
|
|
1733
|
+
o.trim() && (ae(i, o) || a.push({
|
|
1734
1734
|
ruleId: "label-content-name-mismatch",
|
|
1735
|
-
selector: m(
|
|
1736
|
-
html: d(
|
|
1735
|
+
selector: m(e),
|
|
1736
|
+
html: d(e),
|
|
1737
1737
|
impact: "serious",
|
|
1738
1738
|
message: `Accessible name "${i}" does not contain visible label "${o.trim()}".`
|
|
1739
1739
|
}));
|
|
@@ -1748,22 +1748,22 @@ const St = {
|
|
|
1748
1748
|
description: "Form elements should not use title attribute as the only accessible name.",
|
|
1749
1749
|
guidance: "The title attribute is unreliable as a label because it only appears on hover/focus (not visible to touch users) and is often ignored by assistive technologies. Use a visible <label> element, aria-label, or aria-labelledby instead. Title can supplement a label but should not replace it.",
|
|
1750
1750
|
prompt: "The title attribute text should be moved to a visible <label> element or aria-label. Show what text to use based on the current title.",
|
|
1751
|
-
run(
|
|
1751
|
+
run(t) {
|
|
1752
1752
|
var i, n, r, o;
|
|
1753
|
-
const a = [],
|
|
1753
|
+
const a = [], e = t.querySelectorAll(
|
|
1754
1754
|
'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]), textarea, select'
|
|
1755
1755
|
);
|
|
1756
|
-
for (const s of
|
|
1756
|
+
for (const s of e) {
|
|
1757
1757
|
if (g(s)) continue;
|
|
1758
|
-
const l = s.hasAttribute("title") && ((i = s.getAttribute("title")) == null ? void 0 : i.trim()),
|
|
1758
|
+
const l = s.hasAttribute("title") && ((i = s.getAttribute("title")) == null ? void 0 : i.trim()), h = s.hasAttribute("aria-label") && ((n = s.getAttribute("aria-label")) == null ? void 0 : n.trim()), c = s.hasAttribute("aria-labelledby");
|
|
1759
1759
|
let u = !1;
|
|
1760
|
-
const
|
|
1761
|
-
if (
|
|
1762
|
-
const f = s.ownerDocument.querySelector(`label[for="${CSS.escape(
|
|
1760
|
+
const p = s.id;
|
|
1761
|
+
if (p) {
|
|
1762
|
+
const f = s.ownerDocument.querySelector(`label[for="${CSS.escape(p)}"]`);
|
|
1763
1763
|
(r = f == null ? void 0 : f.textContent) != null && r.trim() && (u = !0);
|
|
1764
1764
|
}
|
|
1765
1765
|
const b = s.closest("label");
|
|
1766
|
-
(o = b == null ? void 0 : b.textContent) != null && o.trim() && (u = !0), l && !
|
|
1766
|
+
(o = b == null ? void 0 : b.textContent) != null && o.trim() && (u = !0), l && !h && !c && !u && a.push({
|
|
1767
1767
|
ruleId: "label-title-only",
|
|
1768
1768
|
selector: m(s),
|
|
1769
1769
|
html: d(s),
|
|
@@ -1785,7 +1785,7 @@ const St = {
|
|
|
1785
1785
|
tags: ["best-practice"],
|
|
1786
1786
|
guidance: "Positive tabindex values force elements to the front of the tab order regardless of DOM position, creating unpredictable navigation for keyboard users. Use tabindex='0' to add elements to the natural tab order, or tabindex='-1' to make elements programmatically focusable but not in tab order. Rely on DOM order for tab sequence.",
|
|
1787
1787
|
prompt: "Change the positive tabindex value to tabindex='0' and rely on DOM order for tab sequence instead."
|
|
1788
|
-
}, Tt =
|
|
1788
|
+
}, Tt = I(It), Et = /* @__PURE__ */ new Set([
|
|
1789
1789
|
"div",
|
|
1790
1790
|
"span",
|
|
1791
1791
|
"p",
|
|
@@ -1820,15 +1820,15 @@ const St = {
|
|
|
1820
1820
|
description: "Elements that receive keyboard focus must have an appropriate role so assistive technologies can convey their purpose. Non-interactive elements with tabindex='0' need a valid interactive ARIA role.",
|
|
1821
1821
|
guidance: "When adding tabindex='0' to non-interactive elements like <div> or <span>, screen readers announce them generically. Add an appropriate role (button, link, tab, etc.) so users understand the element's purpose. Also add keyboard event handlers (Enter/Space for buttons, Enter for links). Consider using native interactive elements instead.",
|
|
1822
1822
|
prompt: "Based on the element's apparent purpose, suggest adding an appropriate role attribute (button, link, etc.) or converting to a native interactive element.",
|
|
1823
|
-
run(
|
|
1823
|
+
run(t) {
|
|
1824
1824
|
const a = [];
|
|
1825
|
-
for (const
|
|
1826
|
-
const i =
|
|
1825
|
+
for (const e of t.querySelectorAll('[tabindex="0"]')) {
|
|
1826
|
+
const i = e.tagName.toLowerCase();
|
|
1827
1827
|
if (!Et.has(i)) continue;
|
|
1828
|
-
|
|
1828
|
+
e.getAttribute("role") || a.push({
|
|
1829
1829
|
ruleId: "focus-order-semantics",
|
|
1830
|
-
selector: m(
|
|
1831
|
-
html: d(
|
|
1830
|
+
selector: m(e),
|
|
1831
|
+
html: d(e),
|
|
1832
1832
|
impact: "moderate",
|
|
1833
1833
|
message: `Non-interactive <${i}> with tabindex="0" has no interactive role.`
|
|
1834
1834
|
});
|
|
@@ -1878,24 +1878,24 @@ const St = {
|
|
|
1878
1878
|
tree: /* @__PURE__ */ new Set(["treeitem"]),
|
|
1879
1879
|
treegrid: /* @__PURE__ */ new Set(["gridcell", "row", "columnheader", "rowheader", "treeitem"])
|
|
1880
1880
|
};
|
|
1881
|
-
function Nt(
|
|
1881
|
+
function Nt(t, a) {
|
|
1882
1882
|
var n, r, o;
|
|
1883
|
-
const
|
|
1884
|
-
return !
|
|
1883
|
+
const e = (n = t.getAttribute("role")) == null ? void 0 : n.toLowerCase(), i = (r = a.getAttribute("role")) == null ? void 0 : r.toLowerCase();
|
|
1884
|
+
return !e || !i ? !1 : ((o = Rt[e]) == null ? void 0 : o.has(i)) ?? !1;
|
|
1885
1885
|
}
|
|
1886
|
-
function $t(
|
|
1886
|
+
function $t(t) {
|
|
1887
1887
|
var n;
|
|
1888
|
-
const a =
|
|
1888
|
+
const a = t.tagName.toLowerCase();
|
|
1889
1889
|
if (Lt.has(a))
|
|
1890
|
-
return a === "a" && !
|
|
1891
|
-
const
|
|
1892
|
-
if (
|
|
1893
|
-
const i =
|
|
1894
|
-
return i !== null && i !== "-1" ||
|
|
1890
|
+
return a === "a" && !t.hasAttribute("href") ? !1 : a === "audio" || a === "video" ? t.hasAttribute("controls") : !(a === "img" && !t.hasAttribute("usemap") || a === "input" && t.type === "hidden" || t.disabled);
|
|
1891
|
+
const e = (n = t.getAttribute("role")) == null ? void 0 : n.toLowerCase();
|
|
1892
|
+
if (e && qt.has(e)) return !0;
|
|
1893
|
+
const i = t.getAttribute("tabindex");
|
|
1894
|
+
return i !== null && i !== "-1" || t.getAttribute("contenteditable") === "true";
|
|
1895
1895
|
}
|
|
1896
|
-
function Mt(
|
|
1897
|
-
const a =
|
|
1898
|
-
return !!(a === "a" &&
|
|
1896
|
+
function Mt(t) {
|
|
1897
|
+
const a = t.tagName.toLowerCase();
|
|
1898
|
+
return !!(a === "a" && t.hasAttribute("href") || a === "button" && !t.disabled);
|
|
1899
1899
|
}
|
|
1900
1900
|
const Ht = {
|
|
1901
1901
|
id: "nested-interactive",
|
|
@@ -1904,10 +1904,10 @@ const Ht = {
|
|
|
1904
1904
|
description: "Interactive controls must not be nested inside each other.",
|
|
1905
1905
|
guidance: "Nesting interactive elements (like a button inside a link, or a link inside a button) creates unpredictable behavior and confuses assistive technologies. The browser may remove the inner element from the accessibility tree. Restructure the HTML so interactive elements are siblings, not nested. If you need a clickable card, use CSS and JavaScript rather than nesting.",
|
|
1906
1906
|
prompt: "Identify which elements are nested and suggest restructuring them as siblings instead.",
|
|
1907
|
-
run(
|
|
1908
|
-
const a = [],
|
|
1909
|
-
if (!
|
|
1910
|
-
const n = (
|
|
1907
|
+
run(t) {
|
|
1908
|
+
const a = [], e = t.body ?? t;
|
|
1909
|
+
if (!e) return a;
|
|
1910
|
+
const n = (t.body ? t : t.ownerDocument).createTreeWalker(e, NodeFilter.SHOW_ELEMENT), r = [];
|
|
1911
1911
|
let o = n.currentNode;
|
|
1912
1912
|
for (; o; ) {
|
|
1913
1913
|
for (; r.length > 0 && !r[r.length - 1].contains(o); )
|
|
@@ -1936,22 +1936,22 @@ const Ht = {
|
|
|
1936
1936
|
description: "Scrollable regions must be keyboard accessible.",
|
|
1937
1937
|
guidance: "Content that scrolls must be accessible to keyboard users. If a region has overflow:scroll or overflow:auto and contains scrollable content, it needs either tabindex='0' to be focusable, or it must contain focusable elements. Without this, keyboard users cannot scroll the content.",
|
|
1938
1938
|
prompt: "Explain how to make this scrollable region keyboard accessible.",
|
|
1939
|
-
run(
|
|
1940
|
-
var
|
|
1939
|
+
run(t) {
|
|
1940
|
+
var e;
|
|
1941
1941
|
const a = [];
|
|
1942
|
-
for (const i of
|
|
1942
|
+
for (const i of t.querySelectorAll("*")) {
|
|
1943
1943
|
if (g(i) || !(i instanceof HTMLElement)) continue;
|
|
1944
1944
|
const n = i.tagName.toLowerCase();
|
|
1945
1945
|
if (n === "body" || n === "html") continue;
|
|
1946
1946
|
const r = i.getAttribute("role");
|
|
1947
1947
|
if (r === "presentation" || r === "none") continue;
|
|
1948
|
-
const o =
|
|
1948
|
+
const o = w(i), s = o.overflowX, l = o.overflowY;
|
|
1949
1949
|
if (!(s === "scroll" || s === "auto" || l === "scroll" || l === "auto")) continue;
|
|
1950
1950
|
if (i.scrollHeight > 0 || i.clientHeight > 0) {
|
|
1951
1951
|
const b = i.scrollHeight - i.clientHeight, f = i.scrollWidth - i.clientWidth;
|
|
1952
1952
|
if (b <= 0 && f <= 0 || b < 14 && f < 14 || i.clientWidth < 64 && i.clientHeight < 64) continue;
|
|
1953
1953
|
} else {
|
|
1954
|
-
const b = o.height !== "" || o.maxHeight !== "", f = ((
|
|
1954
|
+
const b = o.height !== "" || o.maxHeight !== "", f = ((e = i.textContent) == null ? void 0 : e.trim().length) ?? 0;
|
|
1955
1955
|
if (!b || f <= 50) continue;
|
|
1956
1956
|
}
|
|
1957
1957
|
const u = i.getAttribute("tabindex");
|
|
@@ -1975,17 +1975,17 @@ const Ht = {
|
|
|
1975
1975
|
description: "Accesskey attribute values must be unique.",
|
|
1976
1976
|
guidance: "When multiple elements share the same accesskey, browser behavior becomes unpredictable - usually only the first element is activated. Ensure each accesskey value is unique within the page. Also consider that accesskeys can conflict with browser and screen reader shortcuts, so use them sparingly.",
|
|
1977
1977
|
prompt: "Suggest removing or changing this duplicate accesskey to a unique value.",
|
|
1978
|
-
run(
|
|
1978
|
+
run(t) {
|
|
1979
1979
|
var i;
|
|
1980
|
-
const a = [],
|
|
1981
|
-
for (const n of
|
|
1980
|
+
const a = [], e = /* @__PURE__ */ new Map();
|
|
1981
|
+
for (const n of t.querySelectorAll("[accesskey]")) {
|
|
1982
1982
|
if (g(n)) continue;
|
|
1983
1983
|
const r = (i = n.getAttribute("accesskey")) == null ? void 0 : i.trim().toLowerCase();
|
|
1984
1984
|
if (!r) continue;
|
|
1985
|
-
const o =
|
|
1986
|
-
o.push(n),
|
|
1985
|
+
const o = e.get(r) || [];
|
|
1986
|
+
o.push(n), e.set(r, o);
|
|
1987
1987
|
}
|
|
1988
|
-
for (const [n, r] of
|
|
1988
|
+
for (const [n, r] of e)
|
|
1989
1989
|
if (r.length > 1)
|
|
1990
1990
|
for (const o of r.slice(1))
|
|
1991
1991
|
a.push({
|
|
@@ -2005,10 +2005,10 @@ const Ht = {
|
|
|
2005
2005
|
description: "Heading levels should increase by one; skipping levels (e.g. h2 to h4) makes navigation harder.",
|
|
2006
2006
|
guidance: "Screen reader users navigate by headings to understand page structure. Skipping levels (h2 to h4) suggests missing content and creates confusion. Start with h1 for the page title, then use h2 for main sections, h3 for subsections, etc. You can go back up (h3 to h2) when starting a new section.",
|
|
2007
2007
|
prompt: "State which heading level was expected and suggest changing this heading to the appropriate level.",
|
|
2008
|
-
run(
|
|
2009
|
-
const a = [],
|
|
2008
|
+
run(t) {
|
|
2009
|
+
const a = [], e = t.querySelectorAll("h1, h2, h3, h4, h5, h6, [role='heading']");
|
|
2010
2010
|
let i = 0, n = null;
|
|
2011
|
-
for (const r of
|
|
2011
|
+
for (const r of e) {
|
|
2012
2012
|
if (g(r)) continue;
|
|
2013
2013
|
let o;
|
|
2014
2014
|
r.hasAttribute("aria-level") ? o = parseInt(r.getAttribute("aria-level"), 10) : o = parseInt(r.tagName[1], 10), i > 0 && o > i + 1 && a.push({
|
|
@@ -2022,7 +2022,7 @@ const Ht = {
|
|
|
2022
2022
|
}
|
|
2023
2023
|
return a;
|
|
2024
2024
|
}
|
|
2025
|
-
}, $ = 'article, aside, main, nav, section, [role="article"], [role="complementary"], [role="main"], [role="navigation"], [role="region"]',
|
|
2025
|
+
}, $ = 'article, aside, main, nav, section, [role="article"], [role="complementary"], [role="main"], [role="navigation"], [role="region"]', ie = 'main, [role="main"], header, [role="banner"], footer, [role="contentinfo"], nav, [role="navigation"], aside, [role="complementary"], section[aria-label], section[aria-labelledby], [role="region"][aria-label], [role="region"][aria-labelledby], form[aria-label], form[aria-labelledby], [role="form"][aria-label], [role="form"][aria-labelledby], [role="search"]', Ot = {
|
|
2026
2026
|
id: "landmark-one-main",
|
|
2027
2027
|
wcag: [],
|
|
2028
2028
|
level: "A",
|
|
@@ -2030,18 +2030,18 @@ const Ht = {
|
|
|
2030
2030
|
description: "Page should have exactly one main landmark.",
|
|
2031
2031
|
guidance: "The main landmark contains the primary content of the page. Screen readers allow users to jump directly to main content. Use a single <main> element (or role='main') to wrap the central content, excluding headers, footers, and navigation.",
|
|
2032
2032
|
prompt: "Identify the primary content area and explain how to wrap it in a <main> element.",
|
|
2033
|
-
run(
|
|
2034
|
-
const a =
|
|
2033
|
+
run(t) {
|
|
2034
|
+
const a = t.querySelectorAll('main, [role="main"]');
|
|
2035
2035
|
return a.length === 0 ? [{
|
|
2036
2036
|
ruleId: "landmark-one-main",
|
|
2037
2037
|
selector: "html",
|
|
2038
2038
|
html: "<html>",
|
|
2039
2039
|
impact: "moderate",
|
|
2040
2040
|
message: "Page has no main landmark."
|
|
2041
|
-
}] : a.length > 1 ? Array.from(a).slice(1).map((
|
|
2041
|
+
}] : a.length > 1 ? Array.from(a).slice(1).map((e) => ({
|
|
2042
2042
|
ruleId: "landmark-one-main",
|
|
2043
|
-
selector: m(
|
|
2044
|
-
html: d(
|
|
2043
|
+
selector: m(e),
|
|
2044
|
+
html: d(e),
|
|
2045
2045
|
impact: "moderate",
|
|
2046
2046
|
message: "Page has multiple main landmarks."
|
|
2047
2047
|
})) : [];
|
|
@@ -2054,8 +2054,8 @@ const Ht = {
|
|
|
2054
2054
|
description: "Page should not have more than one banner landmark.",
|
|
2055
2055
|
guidance: "The banner landmark (typically <header>) identifies site-oriented content like logos and search. Only one top-level banner is allowed per page. If you need multiple headers, nest them inside sectioning elements (article, section, aside) where they become scoped headers rather than page-level banners.",
|
|
2056
2056
|
prompt: "Explain whether to remove this duplicate banner or nest it inside a sectioning element.",
|
|
2057
|
-
run(
|
|
2058
|
-
const a = [],
|
|
2057
|
+
run(t) {
|
|
2058
|
+
const a = [], e = t.querySelectorAll('header, [role="banner"]'), i = Array.from(e).filter((n) => !n.closest($));
|
|
2059
2059
|
return i.length > 1 && i.slice(1).forEach(
|
|
2060
2060
|
(n) => a.push({
|
|
2061
2061
|
ruleId: "landmark-no-duplicate-banner",
|
|
@@ -2074,8 +2074,8 @@ const Ht = {
|
|
|
2074
2074
|
description: "Page should not have more than one contentinfo landmark.",
|
|
2075
2075
|
guidance: "The contentinfo landmark (typically <footer>) contains information about the page like copyright and contact info. Only one top-level contentinfo is allowed per page. Nest additional footers inside sectioning elements to scope them.",
|
|
2076
2076
|
prompt: "Explain whether to remove this duplicate footer or nest it inside a sectioning element.",
|
|
2077
|
-
run(
|
|
2078
|
-
const a = [],
|
|
2077
|
+
run(t) {
|
|
2078
|
+
const a = [], e = t.querySelectorAll('footer, [role="contentinfo"]'), i = Array.from(e).filter((n) => !n.closest($));
|
|
2079
2079
|
return i.length > 1 && i.slice(1).forEach(
|
|
2080
2080
|
(n) => a.push({
|
|
2081
2081
|
ruleId: "landmark-no-duplicate-contentinfo",
|
|
@@ -2094,9 +2094,9 @@ const Ht = {
|
|
|
2094
2094
|
description: "Page should not have more than one main landmark.",
|
|
2095
2095
|
guidance: "Only one main landmark should exist per page. The main landmark identifies the primary content area. If you have multiple content sections, use <section> with appropriate headings instead of multiple main elements.",
|
|
2096
2096
|
prompt: "Explain which main landmark to keep and how to restructure the duplicate.",
|
|
2097
|
-
run(
|
|
2098
|
-
const a = [],
|
|
2099
|
-
return
|
|
2097
|
+
run(t) {
|
|
2098
|
+
const a = [], e = t.querySelectorAll('main, [role="main"]');
|
|
2099
|
+
return e.length > 1 && Array.from(e).slice(1).forEach(
|
|
2100
2100
|
(i) => a.push({
|
|
2101
2101
|
ruleId: "landmark-no-duplicate-main",
|
|
2102
2102
|
selector: m(i),
|
|
@@ -2114,9 +2114,9 @@ const Ht = {
|
|
|
2114
2114
|
description: "Banner landmark should not be nested within another landmark.",
|
|
2115
2115
|
guidance: "The banner landmark should be a top-level landmark, not nested inside article, aside, main, nav, or section. If a header is inside these elements, it automatically becomes a generic header rather than a banner. Remove explicit role='banner' from nested headers or restructure the page.",
|
|
2116
2116
|
prompt: "Explain why this banner is incorrectly nested and how to fix it.",
|
|
2117
|
-
run(
|
|
2118
|
-
const a = [],
|
|
2119
|
-
for (const i of
|
|
2117
|
+
run(t) {
|
|
2118
|
+
const a = [], e = t.querySelectorAll('[role="banner"]');
|
|
2119
|
+
for (const i of e)
|
|
2120
2120
|
i.closest($) && a.push({
|
|
2121
2121
|
ruleId: "landmark-banner-is-top-level",
|
|
2122
2122
|
selector: m(i),
|
|
@@ -2134,9 +2134,9 @@ const Ht = {
|
|
|
2134
2134
|
description: "Contentinfo landmark should not be nested within another landmark.",
|
|
2135
2135
|
guidance: "The contentinfo landmark should be a top-level landmark. A footer inside article, aside, main, nav, or section becomes a scoped footer, not a contentinfo landmark. Remove explicit role='contentinfo' from nested footers or move the footer outside sectioning elements.",
|
|
2136
2136
|
prompt: "Explain why this contentinfo is incorrectly nested and how to fix it.",
|
|
2137
|
-
run(
|
|
2138
|
-
const a = [],
|
|
2139
|
-
for (const i of
|
|
2137
|
+
run(t) {
|
|
2138
|
+
const a = [], e = t.querySelectorAll('[role="contentinfo"]');
|
|
2139
|
+
for (const i of e)
|
|
2140
2140
|
i.closest($) && a.push({
|
|
2141
2141
|
ruleId: "landmark-contentinfo-is-top-level",
|
|
2142
2142
|
selector: m(i),
|
|
@@ -2154,9 +2154,9 @@ const Ht = {
|
|
|
2154
2154
|
description: "Main landmark should not be nested within another landmark.",
|
|
2155
2155
|
guidance: "The main landmark must be a top-level landmark since it represents the primary content of the page. Do not nest <main> or role='main' inside article, aside, nav, or section elements.",
|
|
2156
2156
|
prompt: "Explain why the main landmark must be top-level and where to move it.",
|
|
2157
|
-
run(
|
|
2158
|
-
const a = [],
|
|
2159
|
-
for (const i of
|
|
2157
|
+
run(t) {
|
|
2158
|
+
const a = [], e = t.querySelectorAll('main, [role="main"]');
|
|
2159
|
+
for (const i of e) {
|
|
2160
2160
|
const n = i.parentElement;
|
|
2161
2161
|
n != null && n.closest('article, aside, nav, section, [role="article"], [role="complementary"], [role="navigation"], [role="region"]') && a.push({
|
|
2162
2162
|
ruleId: "landmark-main-is-top-level",
|
|
@@ -2176,9 +2176,9 @@ const Ht = {
|
|
|
2176
2176
|
description: "Aside (complementary) landmark should be top-level or directly inside main.",
|
|
2177
2177
|
guidance: "The complementary landmark (aside) should be top-level or a direct child of main. Nesting aside deep within other landmarks reduces its discoverability for screen reader users navigating by landmarks.",
|
|
2178
2178
|
prompt: "Explain why this aside should be repositioned and suggest where to move it.",
|
|
2179
|
-
run(
|
|
2180
|
-
const a = [],
|
|
2181
|
-
for (const i of
|
|
2179
|
+
run(t) {
|
|
2180
|
+
const a = [], e = t.querySelectorAll('aside, [role="complementary"]');
|
|
2181
|
+
for (const i of e) {
|
|
2182
2182
|
const n = i.parentElement;
|
|
2183
2183
|
n && !n.matches('body, main, [role="main"]') && i.closest('article, nav, section, [role="article"], [role="navigation"], [role="region"]') && a.push({
|
|
2184
2184
|
ruleId: "landmark-complementary-is-top-level",
|
|
@@ -2198,28 +2198,28 @@ const Ht = {
|
|
|
2198
2198
|
description: "Landmarks should have unique labels when there are multiple of the same type.",
|
|
2199
2199
|
guidance: "When a page has multiple landmarks of the same type (e.g., multiple nav elements), each should have a unique accessible name via aria-label or aria-labelledby. This helps screen reader users distinguish between them (e.g., 'Main navigation' vs 'Footer navigation').",
|
|
2200
2200
|
prompt: "Suggest a unique aria-label that distinguishes this landmark based on its purpose.",
|
|
2201
|
-
run(
|
|
2202
|
-
const a = [],
|
|
2201
|
+
run(t) {
|
|
2202
|
+
const a = [], e = [
|
|
2203
2203
|
{ selector: 'nav, [role="navigation"]', type: "navigation" },
|
|
2204
2204
|
{ selector: 'aside, [role="complementary"]', type: "complementary" },
|
|
2205
2205
|
{ selector: 'section[aria-label], section[aria-labelledby], [role="region"]', type: "region" },
|
|
2206
2206
|
{ selector: 'form[aria-label], form[aria-labelledby], [role="form"], [role="search"]', type: "form" }
|
|
2207
2207
|
];
|
|
2208
|
-
for (const { selector: i, type: n } of
|
|
2209
|
-
const r = Array.from(
|
|
2208
|
+
for (const { selector: i, type: n } of e) {
|
|
2209
|
+
const r = Array.from(t.querySelectorAll(i)).filter((s) => !g(s));
|
|
2210
2210
|
if (r.length <= 1) continue;
|
|
2211
2211
|
const o = /* @__PURE__ */ new Map();
|
|
2212
2212
|
for (const s of r) {
|
|
2213
|
-
const l = v(s).toLowerCase() || "",
|
|
2214
|
-
|
|
2213
|
+
const l = v(s).toLowerCase() || "", h = o.get(l) || [];
|
|
2214
|
+
h.push(s), o.set(l, h);
|
|
2215
2215
|
}
|
|
2216
2216
|
for (const [s, l] of o)
|
|
2217
2217
|
if (l.length > 1)
|
|
2218
|
-
for (const
|
|
2218
|
+
for (const h of l.slice(1))
|
|
2219
2219
|
a.push({
|
|
2220
2220
|
ruleId: "landmark-unique",
|
|
2221
|
-
selector: m(
|
|
2222
|
-
html: d(
|
|
2221
|
+
selector: m(h),
|
|
2222
|
+
html: d(h),
|
|
2223
2223
|
impact: "moderate",
|
|
2224
2224
|
message: s ? `Multiple ${n} landmarks have the same label "${s}".` : `Multiple ${n} landmarks have no label. Add unique aria-label attributes.`
|
|
2225
2225
|
});
|
|
@@ -2234,14 +2234,14 @@ const Ht = {
|
|
|
2234
2234
|
description: "All page content should be contained within landmarks.",
|
|
2235
2235
|
guidance: "Screen reader users navigate pages by landmarks. Content outside landmarks is harder to find and understand. Wrap all visible content in appropriate landmarks: <header>, <nav>, <main>, <aside>, <footer>, or <section> with a label. Skip links may exist outside landmarks.",
|
|
2236
2236
|
prompt: "Based on the content, suggest which landmark element would be most appropriate.",
|
|
2237
|
-
run(
|
|
2237
|
+
run(t) {
|
|
2238
2238
|
var i;
|
|
2239
|
-
const a = [],
|
|
2240
|
-
if (!
|
|
2241
|
-
for (const n of
|
|
2239
|
+
const a = [], e = t.body;
|
|
2240
|
+
if (!e) return [];
|
|
2241
|
+
for (const n of e.children) {
|
|
2242
2242
|
if (g(n) || n instanceof HTMLScriptElement || n instanceof HTMLStyleElement || n.tagName === "NOSCRIPT" || n instanceof HTMLElement && n.hidden || n.matches('a[href^="#"]')) continue;
|
|
2243
|
-
const r = n.matches(
|
|
2244
|
-
!r && o && (n.querySelector(
|
|
2243
|
+
const r = n.matches(ie), o = (i = n.textContent) == null ? void 0 : i.trim();
|
|
2244
|
+
!r && o && (n.querySelector(ie) || a.push({
|
|
2245
2245
|
ruleId: "region",
|
|
2246
2246
|
selector: m(n),
|
|
2247
2247
|
html: d(n),
|
|
@@ -2262,22 +2262,22 @@ const Ht = {
|
|
|
2262
2262
|
level: "A",
|
|
2263
2263
|
guidance: "Screen readers announce list structure ('list with 5 items') based on proper markup. Placing non-<li> elements directly inside <ul> or <ol> breaks this structure. Wrap content in <li> elements, or if you need wrapper divs for styling, restructure your CSS to style the <li> elements directly.",
|
|
2264
2264
|
prompt: "Explain how to restructure this element within the list properly."
|
|
2265
|
-
}, Kt =
|
|
2265
|
+
}, Kt = I(Xt), Jt = {
|
|
2266
2266
|
id: "dlitem",
|
|
2267
2267
|
wcag: ["1.3.1"],
|
|
2268
2268
|
level: "A",
|
|
2269
2269
|
description: "<dt> and <dd> elements must be contained in a <dl>.",
|
|
2270
2270
|
guidance: "Definition terms (<dt>) and definitions (<dd>) only have semantic meaning inside a definition list (<dl>). Outside of <dl>, they're treated as generic text. Wrap related <dt> and <dd> pairs in a <dl> element to convey the term/definition relationship to assistive technologies.",
|
|
2271
2271
|
prompt: "Explain how to properly structure this term/definition content.",
|
|
2272
|
-
run(
|
|
2272
|
+
run(t) {
|
|
2273
2273
|
const a = [];
|
|
2274
|
-
for (const
|
|
2275
|
-
(!
|
|
2274
|
+
for (const e of t.querySelectorAll("dt, dd"))
|
|
2275
|
+
(!e.parentElement || e.parentElement.tagName.toLowerCase() !== "dl") && a.push({
|
|
2276
2276
|
ruleId: "dlitem",
|
|
2277
|
-
selector: m(
|
|
2278
|
-
html: d(
|
|
2277
|
+
selector: m(e),
|
|
2278
|
+
html: d(e),
|
|
2279
2279
|
impact: "serious",
|
|
2280
|
-
message: `<${
|
|
2280
|
+
message: `<${e.tagName.toLowerCase()}> is not contained in a <dl>.`
|
|
2281
2281
|
});
|
|
2282
2282
|
return a;
|
|
2283
2283
|
}
|
|
@@ -2292,22 +2292,22 @@ const Ht = {
|
|
|
2292
2292
|
level: "A",
|
|
2293
2293
|
guidance: "Definition lists have strict content requirements. Only <dt> (terms), <dd> (definitions), and <div> (for grouping dt/dd pairs) are valid children. Other elements break the list structure for screen readers. Move invalid elements outside the <dl>, or restructure using proper definition list markup.",
|
|
2294
2294
|
prompt: "Explain whether to move this element outside the <dl> or convert it to dt/dd."
|
|
2295
|
-
}, Zt =
|
|
2295
|
+
}, Zt = I(Qt), ea = {
|
|
2296
2296
|
id: "listitem",
|
|
2297
2297
|
wcag: ["1.3.1"],
|
|
2298
2298
|
level: "A",
|
|
2299
2299
|
description: "<li> elements must be contained in a <ul>, <ol>, or <menu>.",
|
|
2300
2300
|
guidance: "List items (<li>) only have semantic meaning inside a list container (<ul>, <ol>, or <menu>). Outside of these containers, assistive technologies cannot convey the list relationship. Wrap <li> elements in the appropriate list container.",
|
|
2301
2301
|
prompt: "Explain that this <li> must be placed inside a <ul>, <ol>, or <menu> element.",
|
|
2302
|
-
run(
|
|
2303
|
-
var
|
|
2302
|
+
run(t) {
|
|
2303
|
+
var e;
|
|
2304
2304
|
const a = [];
|
|
2305
|
-
for (const i of
|
|
2305
|
+
for (const i of t.querySelectorAll("li")) {
|
|
2306
2306
|
if (g(i)) continue;
|
|
2307
2307
|
const n = i.parentElement;
|
|
2308
2308
|
if (!n) continue;
|
|
2309
2309
|
const r = n.tagName.toLowerCase();
|
|
2310
|
-
r === "ul" || r === "ol" || r === "menu" || ((
|
|
2310
|
+
r === "ul" || r === "ol" || r === "menu" || ((e = n.getAttribute("role")) == null ? void 0 : e.trim().toLowerCase()) === "list" || a.push({
|
|
2311
2311
|
ruleId: "listitem",
|
|
2312
2312
|
selector: m(i),
|
|
2313
2313
|
html: d(i),
|
|
@@ -2324,16 +2324,16 @@ const Ht = {
|
|
|
2324
2324
|
description: "Documents must have a <title> element to provide users with an overview of content.",
|
|
2325
2325
|
guidance: "Screen reader users rely on page titles to identify and navigate between tabs/windows. Add a descriptive <title> element in <head> that summarizes the page purpose. Keep titles unique across the site, placing specific content before the site name (e.g., 'Contact Us - Acme Corp').",
|
|
2326
2326
|
prompt: "The page has no title or an empty title. Suggest a concise, descriptive <title> based on the page content sample in context. Good titles are specific and front-load the unique part: 'Product Details - Store Name' rather than 'Store Name - Product Details'.",
|
|
2327
|
-
run(
|
|
2328
|
-
var
|
|
2329
|
-
const a =
|
|
2330
|
-
if (!a || !((
|
|
2327
|
+
run(t) {
|
|
2328
|
+
var e, i, n;
|
|
2329
|
+
const a = t.querySelector("title");
|
|
2330
|
+
if (!a || !((e = a.textContent) != null && e.trim())) {
|
|
2331
2331
|
let r;
|
|
2332
|
-
const o =
|
|
2332
|
+
const o = t.querySelector("h1");
|
|
2333
2333
|
if ((i = o == null ? void 0 : o.textContent) != null && i.trim())
|
|
2334
2334
|
r = `h1: "${o.textContent.trim().slice(0, 100)}"`;
|
|
2335
|
-
else if (
|
|
2336
|
-
const s = ((n =
|
|
2335
|
+
else if (t.body) {
|
|
2336
|
+
const s = ((n = t.body.textContent) == null ? void 0 : n.trim().replace(/\s+/g, " ")) || "";
|
|
2337
2337
|
s && (r = `Page text: "${s.slice(0, 150)}"`);
|
|
2338
2338
|
}
|
|
2339
2339
|
return [{
|
|
@@ -2355,19 +2355,19 @@ const Ht = {
|
|
|
2355
2355
|
description: "Page must have a mechanism to bypass repeated blocks of content.",
|
|
2356
2356
|
guidance: 'Keyboard users must be able to skip repetitive content like navigation. Provide a skip link at the top of the page that links to the main content (e.g., <a href="#main">Skip to main content</a>), or use a <main> landmark. Screen readers can jump directly to landmarks, so a properly marked-up <main> element satisfies this requirement.',
|
|
2357
2357
|
prompt: 'The page has no mechanism for keyboard users to skip repeated content. The simplest fix is to wrap the primary content area in a <main> element — screen readers can jump directly to it. Alternatively, add a skip link as the first element in <body>: <a href="#main" class="skip-link">Skip to main content</a>, with a matching id on the target element. Use the context to understand what the page is missing.',
|
|
2358
|
-
run(
|
|
2359
|
-
if (
|
|
2358
|
+
run(t) {
|
|
2359
|
+
if (t.querySelector(
|
|
2360
2360
|
'main, [role="main"], nav, [role="navigation"], aside, [role="complementary"], header, [role="banner"], footer, [role="contentinfo"], [role="search"], [role="region"]'
|
|
2361
2361
|
)) return [];
|
|
2362
|
-
const
|
|
2363
|
-
if (
|
|
2364
|
-
const r =
|
|
2362
|
+
const e = t.querySelector('a[href^="#"]');
|
|
2363
|
+
if (e) {
|
|
2364
|
+
const r = e.getAttribute("href");
|
|
2365
2365
|
if (r && r.length > 1) {
|
|
2366
2366
|
const o = r.slice(1);
|
|
2367
|
-
if (
|
|
2367
|
+
if (t.getElementById(o)) return [];
|
|
2368
2368
|
}
|
|
2369
2369
|
}
|
|
2370
|
-
if (
|
|
2370
|
+
if (t.querySelector("h1, h2, h3, [role='heading']")) return [];
|
|
2371
2371
|
const n = [];
|
|
2372
2372
|
return n.push("no landmarks (<main>, <nav>, <header>, <footer>)"), n.push("no skip link"), n.push("no headings"), [{
|
|
2373
2373
|
ruleId: "bypass",
|
|
@@ -2386,19 +2386,19 @@ const Ht = {
|
|
|
2386
2386
|
description: "Page should contain a level-one heading.",
|
|
2387
2387
|
guidance: "A level-one heading (<h1> or role='heading' with aria-level='1') helps users understand the page topic and provides a landmark for screen reader navigation. Each page should have exactly one h1 that describes the main content, typically matching or similar to the page title.",
|
|
2388
2388
|
prompt: "The page has no <h1> heading. Suggest appropriate h1 text based on the page title or content sample in context. The h1 should describe the page's main topic and typically be placed at the start of the main content area.",
|
|
2389
|
-
run(
|
|
2389
|
+
run(t) {
|
|
2390
2390
|
var o, s, l;
|
|
2391
|
-
const a =
|
|
2391
|
+
const a = t.querySelector("h1");
|
|
2392
2392
|
if (a && v(a)) return [];
|
|
2393
|
-
const
|
|
2394
|
-
for (const
|
|
2395
|
-
if (v(
|
|
2396
|
-
const i = [], n = (s = (o =
|
|
2393
|
+
const e = t.querySelectorAll('[role="heading"][aria-level="1"]');
|
|
2394
|
+
for (const h of e)
|
|
2395
|
+
if (v(h)) return [];
|
|
2396
|
+
const i = [], n = (s = (o = t.querySelector("title")) == null ? void 0 : o.textContent) == null ? void 0 : s.trim();
|
|
2397
2397
|
n && i.push(`Page title: "${n}"`);
|
|
2398
|
-
const r =
|
|
2398
|
+
const r = t.querySelector("main");
|
|
2399
2399
|
if (r) {
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2400
|
+
const h = ((l = r.textContent) == null ? void 0 : l.trim().replace(/\s+/g, " ")) || "";
|
|
2401
|
+
h && i.push(`Main content: "${h.slice(0, 100)}"`);
|
|
2402
2402
|
}
|
|
2403
2403
|
return [{
|
|
2404
2404
|
ruleId: "page-has-heading-one",
|
|
@@ -2410,11 +2410,11 @@ const Ht = {
|
|
|
2410
2410
|
}];
|
|
2411
2411
|
}
|
|
2412
2412
|
};
|
|
2413
|
-
function be(
|
|
2414
|
-
if (!(
|
|
2415
|
-
if (
|
|
2416
|
-
const a =
|
|
2417
|
-
return (a === "0" || a === "1") && (
|
|
2413
|
+
function be(t) {
|
|
2414
|
+
if (!(t instanceof HTMLElement)) return !1;
|
|
2415
|
+
if (t.style.display === "none" || t.style.visibility === "hidden") return !0;
|
|
2416
|
+
const a = t.getAttribute("width"), e = t.getAttribute("height");
|
|
2417
|
+
return (a === "0" || a === "1") && (e === "0" || e === "1");
|
|
2418
2418
|
}
|
|
2419
2419
|
const na = {
|
|
2420
2420
|
id: "frame-title",
|
|
@@ -2423,16 +2423,16 @@ const na = {
|
|
|
2423
2423
|
description: "Frames must have an accessible name.",
|
|
2424
2424
|
guidance: "Screen readers announce frame titles when users navigate frames. Add a title attribute to <iframe> and <frame> elements that describes the frame's purpose (e.g., <iframe title='Video player'>). Avoid generic titles like 'frame' or 'iframe'. If the frame is decorative, use aria-hidden='true'.",
|
|
2425
2425
|
prompt: "This iframe has no accessible name. Based on the src URL in context, suggest a descriptive title attribute that tells screen reader users what the frame contains. For example: 'YouTube video player', 'Google Map', 'Payment form', 'Chat widget'. If the frame appears decorative or non-essential, recommend adding aria-hidden='true' instead.",
|
|
2426
|
-
run(
|
|
2426
|
+
run(t) {
|
|
2427
2427
|
const a = [];
|
|
2428
|
-
for (const
|
|
2429
|
-
if (g(
|
|
2430
|
-
if (!v(
|
|
2431
|
-
const n =
|
|
2428
|
+
for (const e of t.querySelectorAll("iframe, frame")) {
|
|
2429
|
+
if (g(e) || be(e)) continue;
|
|
2430
|
+
if (!v(e)) {
|
|
2431
|
+
const n = e.getAttribute("src");
|
|
2432
2432
|
a.push({
|
|
2433
2433
|
ruleId: "frame-title",
|
|
2434
|
-
selector: m(
|
|
2435
|
-
html: d(
|
|
2434
|
+
selector: m(e),
|
|
2435
|
+
html: d(e),
|
|
2436
2436
|
impact: "serious",
|
|
2437
2437
|
message: "Frame is missing an accessible name. Add a title attribute.",
|
|
2438
2438
|
context: n ? `src: "${n}"` : void 0
|
|
@@ -2449,10 +2449,10 @@ const na = {
|
|
|
2449
2449
|
description: "Frame titles should be unique.",
|
|
2450
2450
|
guidance: "When multiple frames have identical titles, screen reader users cannot distinguish between them. Give each frame a unique, descriptive title that explains its specific purpose or content.",
|
|
2451
2451
|
prompt: "Suggest a more specific title to distinguish this frame from others.",
|
|
2452
|
-
run(
|
|
2452
|
+
run(t) {
|
|
2453
2453
|
var n;
|
|
2454
|
-
const a = [],
|
|
2455
|
-
for (const r of
|
|
2454
|
+
const a = [], e = Array.from(t.querySelectorAll("iframe[title], frame[title]")), i = /* @__PURE__ */ new Map();
|
|
2455
|
+
for (const r of e) {
|
|
2456
2456
|
if (g(r) || be(r)) continue;
|
|
2457
2457
|
const o = (n = r.getAttribute("title")) == null ? void 0 : n.trim().toLowerCase();
|
|
2458
2458
|
if (o) {
|
|
@@ -2480,10 +2480,10 @@ const na = {
|
|
|
2480
2480
|
description: "Headings must have discernible text.",
|
|
2481
2481
|
guidance: "Screen reader users navigate pages by headings, so empty headings create confusing navigation points. Ensure all headings contain visible text or accessible names. If a heading is used purely for visual styling, use CSS instead of heading elements.",
|
|
2482
2482
|
prompt: "This heading element has no text content, so screen reader users encounter a blank heading when navigating. Either add descriptive text that summarizes the following section, or if this element is used only for visual styling, replace it with a styled <p> or <div> and use CSS for appearance. The context includes nearby content to help suggest appropriate heading text.",
|
|
2483
|
-
run(
|
|
2483
|
+
run(t) {
|
|
2484
2484
|
var i;
|
|
2485
|
-
const a = [],
|
|
2486
|
-
for (const n of
|
|
2485
|
+
const a = [], e = t.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"]');
|
|
2486
|
+
for (const n of e)
|
|
2487
2487
|
if (!g(n) && !v(n)) {
|
|
2488
2488
|
let r;
|
|
2489
2489
|
const o = n.nextElementSibling;
|
|
@@ -2509,16 +2509,16 @@ const na = {
|
|
|
2509
2509
|
description: "Viewport meta tag must not disable user scaling.",
|
|
2510
2510
|
guidance: "Users with low vision need to zoom content up to 200% or more. Setting user-scalable=no or maximum-scale=1 prevents zooming and fails WCAG. Remove these restrictions. If your layout breaks at high zoom, fix the responsive design rather than preventing zoom.",
|
|
2511
2511
|
prompt: "The viewport meta tag restricts zooming, which prevents low-vision users from enlarging content. Show the current content attribute and a corrected version with the problematic properties removed. Keep other viewport properties (like width=device-width, initial-scale=1) intact — only remove user-scalable=no and maximum-scale restrictions.",
|
|
2512
|
-
run(
|
|
2513
|
-
const a = [],
|
|
2514
|
-
if (!
|
|
2515
|
-
const i =
|
|
2512
|
+
run(t) {
|
|
2513
|
+
const a = [], e = t.querySelector('meta[name="viewport"]');
|
|
2514
|
+
if (!e) return [];
|
|
2515
|
+
const i = e.getAttribute("content") || "", n = i.toLowerCase(), r = n.match(/user-scalable\s*=\s*([^\s,;]+)/i);
|
|
2516
2516
|
if (r) {
|
|
2517
2517
|
const s = r[1], l = parseFloat(s);
|
|
2518
2518
|
(s === "no" || !isNaN(l) && l > -1 && l < 1) && a.push({
|
|
2519
2519
|
ruleId: "meta-viewport",
|
|
2520
|
-
selector: m(
|
|
2521
|
-
html: d(
|
|
2520
|
+
selector: m(e),
|
|
2521
|
+
html: d(e),
|
|
2522
2522
|
impact: "critical",
|
|
2523
2523
|
message: `Viewport disables user scaling (user-scalable=${s}). Remove this restriction.`,
|
|
2524
2524
|
context: `content: "${i}"`
|
|
@@ -2529,8 +2529,8 @@ const na = {
|
|
|
2529
2529
|
const s = o[1], l = s.toLowerCase() === "yes" ? 1 : parseFloat(s);
|
|
2530
2530
|
l < 2 && a.push({
|
|
2531
2531
|
ruleId: "meta-viewport",
|
|
2532
|
-
selector: m(
|
|
2533
|
-
html: d(
|
|
2532
|
+
selector: m(e),
|
|
2533
|
+
html: d(e),
|
|
2534
2534
|
impact: "critical",
|
|
2535
2535
|
message: `Viewport maximum-scale=${l} restricts zooming. Set to at least 2 or remove.`,
|
|
2536
2536
|
context: `content: "${i}"`
|
|
@@ -2545,12 +2545,12 @@ const na = {
|
|
|
2545
2545
|
description: "Meta refresh must not redirect or refresh automatically.",
|
|
2546
2546
|
guidance: "Automatic page refreshes or redirects can disorient users, especially those using screen readers or with cognitive disabilities. They may lose their place or not have time to read content. If a redirect is needed, use a server-side redirect (HTTP 301/302) instead. For timed refreshes, provide user controls.",
|
|
2547
2547
|
prompt: "Explain why meta refresh is problematic and suggest server-side alternatives.",
|
|
2548
|
-
run(
|
|
2549
|
-
for (const a of
|
|
2550
|
-
const
|
|
2548
|
+
run(t) {
|
|
2549
|
+
for (const a of t.querySelectorAll('meta[http-equiv="refresh"]')) {
|
|
2550
|
+
const e = a.getAttribute("content") || "", i = e.match(/^(\d+)/);
|
|
2551
2551
|
if (!i) continue;
|
|
2552
2552
|
const n = parseInt(i[1], 10);
|
|
2553
|
-
if (/^\d+\s*[;,]\s*url\s*=/i.test(
|
|
2553
|
+
if (/^\d+\s*[;,]\s*url\s*=/i.test(e) || /^\d+\s*[;,]\s*['"]?\s*https?:/i.test(e))
|
|
2554
2554
|
return n > 0 && n <= 72e3 ? [{
|
|
2555
2555
|
ruleId: "meta-refresh",
|
|
2556
2556
|
selector: m(a),
|
|
@@ -2580,7 +2580,7 @@ const na = {
|
|
|
2580
2580
|
level: "A",
|
|
2581
2581
|
guidance: "Blinking content can cause seizures in users with photosensitive epilepsy and is distracting for users with attention disorders. The <blink> element is deprecated and should never be used. If you need to draw attention to content, use less intrusive methods like color, borders, or icons.",
|
|
2582
2582
|
prompt: "Suggest static alternatives to the blinking effect."
|
|
2583
|
-
}, ua =
|
|
2583
|
+
}, ua = I(ca), da = {
|
|
2584
2584
|
id: "marquee",
|
|
2585
2585
|
selector: "marquee",
|
|
2586
2586
|
check: { type: "selector-exists" },
|
|
@@ -2591,7 +2591,7 @@ const na = {
|
|
|
2591
2591
|
level: "A",
|
|
2592
2592
|
guidance: "Scrolling or moving content is difficult for many users to read, especially those with cognitive or visual disabilities. The <marquee> element is deprecated. Replace scrolling text with static content. If content must scroll, provide pause/stop controls and ensure it stops after 5 seconds.",
|
|
2593
2593
|
prompt: "Suggest static alternatives or accessible carousel patterns."
|
|
2594
|
-
}, ma =
|
|
2594
|
+
}, ma = I(da), ha = {
|
|
2595
2595
|
id: "p-as-heading",
|
|
2596
2596
|
wcag: [],
|
|
2597
2597
|
level: "A",
|
|
@@ -2599,15 +2599,15 @@ const na = {
|
|
|
2599
2599
|
description: "Paragraphs should not be styled to look like headings.",
|
|
2600
2600
|
guidance: "When paragraphs are styled with bold, large fonts to look like headings, screen reader users miss the semantic structure. Use proper heading elements (h1-h6) instead of styled paragraphs. If you need specific styling, apply CSS to the heading elements while maintaining proper heading hierarchy.",
|
|
2601
2601
|
prompt: "Suggest the appropriate heading level based on the document structure.",
|
|
2602
|
-
run(
|
|
2603
|
-
var
|
|
2602
|
+
run(t) {
|
|
2603
|
+
var e, i;
|
|
2604
2604
|
const a = [];
|
|
2605
|
-
for (const n of
|
|
2605
|
+
for (const n of t.querySelectorAll("p")) {
|
|
2606
2606
|
if (g(n)) continue;
|
|
2607
|
-
const r = n.getAttribute("style") || "", o = /font-weight\s*:\s*(bold|[6-9]00)/i.test(r), s = /font-size\s*:\s*(\d+)\s*(px|em|rem)/i.test(r), l = ((
|
|
2608
|
-
if ((o && s || o &&
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2607
|
+
const r = n.getAttribute("style") || "", o = /font-weight\s*:\s*(bold|[6-9]00)/i.test(r), s = /font-size\s*:\s*(\d+)\s*(px|em|rem)/i.test(r), l = ((e = n.className) == null ? void 0 : e.toLowerCase()) || "", h = /\bh[1-6]\b|\bheading\b/.test(l), c = ((i = n.textContent) == null ? void 0 : i.trim()) || "", u = c.length > 0 && c.length < 50, p = !c.match(/[.!?,;:]$/);
|
|
2608
|
+
if ((o && s || o && h) && u && p) {
|
|
2609
|
+
const y = n.nextElementSibling;
|
|
2610
|
+
y && (y.tagName === "P" || y.tagName === "DIV" || y.tagName === "UL") && a.push({
|
|
2611
2611
|
ruleId: "p-as-heading",
|
|
2612
2612
|
selector: m(n),
|
|
2613
2613
|
html: d(n),
|
|
@@ -2625,14 +2625,14 @@ const na = {
|
|
|
2625
2625
|
description: "ARIA role values must be valid.",
|
|
2626
2626
|
guidance: "Invalid role values are ignored by assistive technologies, meaning the element will not have the intended semantics. Check the spelling and use only roles defined in the WAI-ARIA specification. Common roles include: button, link, navigation, main, dialog, alert, tab, tabpanel, menu, menuitem.",
|
|
2627
2627
|
prompt: "Identify the invalid role and suggest the correct spelling or a valid alternative role that matches the intended purpose.",
|
|
2628
|
-
run(
|
|
2628
|
+
run(t) {
|
|
2629
2629
|
const a = [];
|
|
2630
|
-
for (const
|
|
2631
|
-
const r =
|
|
2630
|
+
for (const e of t.querySelectorAll("[role]")) {
|
|
2631
|
+
const r = e.getAttribute("role").replace(/[\u201C\u201D\u2018\u2019\u00AB\u00BB]/g, "").split(/\s+/).filter(Boolean);
|
|
2632
2632
|
!r.some((s) => Ee(s)) && r.length > 0 && a.push({
|
|
2633
2633
|
ruleId: "aria-roles",
|
|
2634
|
-
selector: m(
|
|
2635
|
-
html: d(
|
|
2634
|
+
selector: m(e),
|
|
2635
|
+
html: d(e),
|
|
2636
2636
|
impact: "critical",
|
|
2637
2637
|
message: `Invalid ARIA role "${r[0]}".`
|
|
2638
2638
|
});
|
|
@@ -2646,8 +2646,8 @@ const na = {
|
|
|
2646
2646
|
description: "ARIA attributes must be valid (correctly spelled).",
|
|
2647
2647
|
guidance: "Misspelled ARIA attributes are ignored by assistive technologies. Check the spelling against the WAI-ARIA specification. Common mistakes: aria-labeledby (should be aria-labelledby), aria-role (should be role), aria-description (valid in ARIA 1.3+).",
|
|
2648
2648
|
prompt: "Identify the misspelled attribute and provide the correct spelling.",
|
|
2649
|
-
run(
|
|
2650
|
-
return
|
|
2649
|
+
run(t) {
|
|
2650
|
+
return G(t).validAttr;
|
|
2651
2651
|
}
|
|
2652
2652
|
}, ba = {
|
|
2653
2653
|
id: "aria-valid-attr-value",
|
|
@@ -2656,8 +2656,8 @@ const na = {
|
|
|
2656
2656
|
description: "ARIA attributes must have valid values.",
|
|
2657
2657
|
guidance: "Each ARIA attribute accepts specific value types. Boolean attributes (aria-hidden, aria-disabled) accept only 'true' or 'false'. Tristate attributes (aria-checked, aria-pressed) also accept 'mixed'. Token attributes (aria-live, aria-autocomplete) accept predefined values. ID reference attributes (aria-labelledby, aria-describedby) must reference existing element IDs.",
|
|
2658
2658
|
prompt: "Show the invalid value and list the valid values for this specific attribute.",
|
|
2659
|
-
run(
|
|
2660
|
-
return
|
|
2659
|
+
run(t) {
|
|
2660
|
+
return G(t).validAttrValue;
|
|
2661
2661
|
}
|
|
2662
2662
|
}, fa = {
|
|
2663
2663
|
checkbox: ["aria-checked"],
|
|
@@ -2680,23 +2680,23 @@ const na = {
|
|
|
2680
2680
|
description: "Elements with ARIA roles must have all required ARIA attributes.",
|
|
2681
2681
|
guidance: "Some ARIA roles require specific attributes to function correctly. For example, checkbox requires aria-checked, slider requires aria-valuenow, heading requires aria-level. Without these attributes, assistive technologies cannot convey the element's state or value to users. Add the missing required attribute with an appropriate value.",
|
|
2682
2682
|
prompt: "State which attribute is required for this role and suggest an appropriate value based on the element's apparent state.",
|
|
2683
|
-
run(
|
|
2683
|
+
run(t) {
|
|
2684
2684
|
const a = [];
|
|
2685
|
-
for (const
|
|
2686
|
-
if (g(
|
|
2687
|
-
const i =
|
|
2688
|
-
if (n && !(i === "checkbox" &&
|
|
2685
|
+
for (const e of t.querySelectorAll("[role]")) {
|
|
2686
|
+
if (g(e) || e instanceof HTMLElement && e.style.display === "none") continue;
|
|
2687
|
+
const i = e.getAttribute("role").trim().toLowerCase(), n = fa[i];
|
|
2688
|
+
if (n && !(i === "checkbox" && e instanceof HTMLInputElement && e.type === "checkbox") && !(i === "radio" && e instanceof HTMLInputElement && e.type === "radio") && !(i === "option" && e instanceof HTMLOptionElement) && !(i === "heading" && /^h[1-6]$/i.test(e.tagName))) {
|
|
2689
2689
|
if (i === "separator") {
|
|
2690
|
-
const r =
|
|
2690
|
+
const r = e.getAttribute("tabindex");
|
|
2691
2691
|
if (!r || r === "-1") continue;
|
|
2692
2692
|
}
|
|
2693
|
-
if (!(
|
|
2693
|
+
if (!(e.tagName.toLowerCase() === "hr" && !e.hasAttribute("role"))) {
|
|
2694
2694
|
for (const r of n)
|
|
2695
|
-
if (!
|
|
2695
|
+
if (!e.hasAttribute(r)) {
|
|
2696
2696
|
a.push({
|
|
2697
2697
|
ruleId: "aria-required-attr",
|
|
2698
|
-
selector: m(
|
|
2699
|
-
html: d(
|
|
2698
|
+
selector: m(e),
|
|
2699
|
+
html: d(e),
|
|
2700
2700
|
impact: "critical",
|
|
2701
2701
|
message: `Role "${i}" requires attribute "${r}".`
|
|
2702
2702
|
});
|
|
@@ -2708,16 +2708,16 @@ const na = {
|
|
|
2708
2708
|
return a;
|
|
2709
2709
|
}
|
|
2710
2710
|
};
|
|
2711
|
-
function
|
|
2711
|
+
function ya(t) {
|
|
2712
2712
|
var r, o, s;
|
|
2713
|
-
const a = [],
|
|
2714
|
-
|
|
2715
|
-
const i =
|
|
2713
|
+
const a = [], e = t.className;
|
|
2714
|
+
e && typeof e == "string" && e.trim() && a.push(`Classes: ${e.trim().slice(0, 100)}`);
|
|
2715
|
+
const i = t.closest("form");
|
|
2716
2716
|
if (i) {
|
|
2717
2717
|
const l = i.getAttribute("aria-label") || ((o = (r = i.querySelector("legend")) == null ? void 0 : r.textContent) == null ? void 0 : o.trim());
|
|
2718
2718
|
l && a.push(`Form: ${l.slice(0, 60)}`);
|
|
2719
2719
|
}
|
|
2720
|
-
const n =
|
|
2720
|
+
const n = t.parentElement;
|
|
2721
2721
|
if (n) {
|
|
2722
2722
|
const l = n.closest("h1, h2, h3, h4, h5, h6") || n.querySelector("h1, h2, h3, h4, h5, h6");
|
|
2723
2723
|
(s = l == null ? void 0 : l.textContent) != null && s.trim() && a.push(`Nearby heading: ${l.textContent.trim().slice(0, 60)}`);
|
|
@@ -2725,26 +2725,26 @@ function wa(e) {
|
|
|
2725
2725
|
return a.length > 0 ? a.join(`
|
|
2726
2726
|
`) : void 0;
|
|
2727
2727
|
}
|
|
2728
|
-
const
|
|
2728
|
+
const wa = {
|
|
2729
2729
|
id: "button-name",
|
|
2730
2730
|
wcag: ["4.1.2"],
|
|
2731
2731
|
level: "A",
|
|
2732
2732
|
description: "Buttons must have discernible text.",
|
|
2733
2733
|
guidance: "Screen reader users need to know what a button does. Add visible text content, aria-label, or aria-labelledby. For icon buttons, use aria-label describing the action (e.g., aria-label='Close'). If the button contains an image, ensure the image has alt text describing the button's action.",
|
|
2734
2734
|
prompt: "Based on the button's content, class, or context, suggest an appropriate aria-label describing the action it performs.",
|
|
2735
|
-
run(
|
|
2735
|
+
run(t) {
|
|
2736
2736
|
const a = [];
|
|
2737
|
-
for (const
|
|
2738
|
-
if (g(
|
|
2739
|
-
const i =
|
|
2740
|
-
if ((i === "none" || i === "presentation") && !(
|
|
2741
|
-
v(
|
|
2737
|
+
for (const e of t.querySelectorAll('button, [role="button"]')) {
|
|
2738
|
+
if (g(e) || T(e)) continue;
|
|
2739
|
+
const i = e.getAttribute("role");
|
|
2740
|
+
if ((i === "none" || i === "presentation") && !(e.matches('button:not([disabled]), [tabindex]:not([tabindex="-1"])') || e.tagName.toLowerCase() === "button" && !e.disabled) || e.getRootNode() instanceof ShadowRoot) continue;
|
|
2741
|
+
v(e) || a.push({
|
|
2742
2742
|
ruleId: "button-name",
|
|
2743
|
-
selector: m(
|
|
2744
|
-
html: d(
|
|
2743
|
+
selector: m(e),
|
|
2744
|
+
html: d(e),
|
|
2745
2745
|
impact: "critical",
|
|
2746
2746
|
message: "Button has no discernible text.",
|
|
2747
|
-
context:
|
|
2747
|
+
context: ya(e)
|
|
2748
2748
|
});
|
|
2749
2749
|
}
|
|
2750
2750
|
return a;
|
|
@@ -2852,21 +2852,21 @@ const ya = {
|
|
|
2852
2852
|
description: "ARIA attributes must be allowed for the element's role.",
|
|
2853
2853
|
guidance: "Each ARIA role supports specific attributes. Using unsupported attributes creates confusion for assistive technologies. Check the ARIA specification for which attributes are valid for each role, or remove the attribute if it's not needed.",
|
|
2854
2854
|
prompt: "The ARIA attribute listed in context is not supported on this element's role. Either remove the attribute (if the behavior it describes isn't needed), or change the element's role to one that supports it. The context lists which attributes ARE allowed on this role — use that to suggest alternatives if applicable.",
|
|
2855
|
-
run(
|
|
2855
|
+
run(t) {
|
|
2856
2856
|
const a = [];
|
|
2857
|
-
for (const
|
|
2858
|
-
if (g(
|
|
2859
|
-
const i = q(
|
|
2857
|
+
for (const e of t.querySelectorAll("[role], [aria-*]")) {
|
|
2858
|
+
if (g(e)) continue;
|
|
2859
|
+
const i = q(e);
|
|
2860
2860
|
if (!i) continue;
|
|
2861
2861
|
const n = Aa[i];
|
|
2862
2862
|
if (n)
|
|
2863
|
-
for (const r of
|
|
2863
|
+
for (const r of e.attributes) {
|
|
2864
2864
|
if (!r.name.startsWith("aria-") || xa.has(r.name) || n.has(r.name)) continue;
|
|
2865
2865
|
const o = n.size > 0 ? [...n].join(", ") : "none (only global ARIA attributes)";
|
|
2866
2866
|
a.push({
|
|
2867
2867
|
ruleId: "aria-allowed-attr",
|
|
2868
|
-
selector: m(
|
|
2869
|
-
html: d(
|
|
2868
|
+
selector: m(e),
|
|
2869
|
+
html: d(e),
|
|
2870
2870
|
impact: "critical",
|
|
2871
2871
|
message: `ARIA attribute "${r.name}" is not allowed on role "${i}".`,
|
|
2872
2872
|
context: `Attribute: ${r.name}="${r.value}", role: ${i}, allowed role-specific attributes: ${o}`
|
|
@@ -2997,17 +2997,17 @@ const ya = {
|
|
|
2997
2997
|
video: /* @__PURE__ */ new Set(["application"]),
|
|
2998
2998
|
wbr: /* @__PURE__ */ new Set(["none", "presentation"])
|
|
2999
2999
|
};
|
|
3000
|
-
function Ia(
|
|
3001
|
-
var
|
|
3002
|
-
const a =
|
|
3000
|
+
function Ia(t) {
|
|
3001
|
+
var e;
|
|
3002
|
+
const a = t.tagName.toLowerCase();
|
|
3003
3003
|
if (ka.has(a))
|
|
3004
3004
|
return "none";
|
|
3005
|
-
if (a === "a" &&
|
|
3005
|
+
if (a === "a" && t.hasAttribute("href"))
|
|
3006
3006
|
return E["a[href]"];
|
|
3007
|
-
if (a === "img" &&
|
|
3007
|
+
if (a === "img" && t.getAttribute("alt") === "")
|
|
3008
3008
|
return E["img[alt='']"];
|
|
3009
3009
|
if (a === "input") {
|
|
3010
|
-
const n = `input[type=${((
|
|
3010
|
+
const n = `input[type=${((e = t.getAttribute("type")) == null ? void 0 : e.toLowerCase()) || "text"}]`;
|
|
3011
3011
|
return n in E ? E[n] : "none";
|
|
3012
3012
|
}
|
|
3013
3013
|
return E[a] || "any";
|
|
@@ -3019,12 +3019,12 @@ const Ta = {
|
|
|
3019
3019
|
description: "ARIA role must be appropriate for the element.",
|
|
3020
3020
|
guidance: "Not all ARIA roles can be applied to all HTML elements. Many elements have implicit roles (e.g., <header> is implicitly banner, <nav> is navigation, <main> is main). Adding an explicit role that matches the implicit role is redundant. Adding a conflicting role breaks semantics. Either remove the role attribute or use a different element.",
|
|
3021
3021
|
prompt: "Consider implicit roles: header=banner, nav=navigation, main=main, footer=contentinfo, aside=complementary, article=article, section=region (when labeled). Explain if this role is redundant (matches implicit) or invalid (conflicts). Suggest removing it or restructuring.",
|
|
3022
|
-
run(
|
|
3023
|
-
var
|
|
3022
|
+
run(t) {
|
|
3023
|
+
var e;
|
|
3024
3024
|
const a = [];
|
|
3025
|
-
for (const i of
|
|
3025
|
+
for (const i of t.querySelectorAll("[role]")) {
|
|
3026
3026
|
if (g(i)) continue;
|
|
3027
|
-
const n = (
|
|
3027
|
+
const n = (e = i.getAttribute("role")) == null ? void 0 : e.trim().toLowerCase();
|
|
3028
3028
|
if (!n) continue;
|
|
3029
3029
|
const r = he(i);
|
|
3030
3030
|
if (r && n === r) continue;
|
|
@@ -3045,7 +3045,7 @@ const Ta = {
|
|
|
3045
3045
|
}
|
|
3046
3046
|
return a;
|
|
3047
3047
|
}
|
|
3048
|
-
},
|
|
3048
|
+
}, ne = {
|
|
3049
3049
|
// Each array is an OR group - at least one of each inner array must be present
|
|
3050
3050
|
combobox: [["listbox", "tree", "grid", "dialog", "textbox"]],
|
|
3051
3051
|
// Must own/contain one of these
|
|
@@ -3074,7 +3074,7 @@ const Ta = {
|
|
|
3074
3074
|
"tablist",
|
|
3075
3075
|
"tree",
|
|
3076
3076
|
"treegrid"
|
|
3077
|
-
]),
|
|
3077
|
+
]), re = {
|
|
3078
3078
|
caption: ["figure", "table", "grid", "treegrid"],
|
|
3079
3079
|
cell: ["row"],
|
|
3080
3080
|
columnheader: ["row"],
|
|
@@ -3090,22 +3090,22 @@ const Ta = {
|
|
|
3090
3090
|
tab: ["tablist"],
|
|
3091
3091
|
treeitem: ["tree", "group"]
|
|
3092
3092
|
};
|
|
3093
|
-
function Ca(
|
|
3093
|
+
function Ca(t, a) {
|
|
3094
3094
|
var o;
|
|
3095
|
-
const
|
|
3095
|
+
const e = ((o = t.getAttribute("aria-owns")) == null ? void 0 : o.split(/\s+/)) || [], i = t.ownerDocument, n = /* @__PURE__ */ new Set();
|
|
3096
3096
|
let r = !1;
|
|
3097
|
-
for (const s of
|
|
3097
|
+
for (const s of t.querySelectorAll("*")) {
|
|
3098
3098
|
if (g(s)) continue;
|
|
3099
3099
|
r = !0;
|
|
3100
3100
|
const l = q(s);
|
|
3101
3101
|
l && n.add(l);
|
|
3102
3102
|
}
|
|
3103
|
-
for (const s of
|
|
3103
|
+
for (const s of e) {
|
|
3104
3104
|
const l = i.getElementById(s);
|
|
3105
3105
|
if (l && !g(l)) {
|
|
3106
3106
|
r = !0;
|
|
3107
|
-
const
|
|
3108
|
-
|
|
3107
|
+
const h = q(l);
|
|
3108
|
+
h && n.add(h);
|
|
3109
3109
|
}
|
|
3110
3110
|
}
|
|
3111
3111
|
if (!r) return "empty";
|
|
@@ -3120,19 +3120,19 @@ const La = {
|
|
|
3120
3120
|
description: "Certain ARIA roles require specific child roles to be present.",
|
|
3121
3121
|
guidance: "Some ARIA roles represent containers that must contain specific child roles for proper semantics. For example, a list must contain listitems, a menu must contain menuitems. Add the required child elements with appropriate roles, or use native HTML elements that provide these semantics implicitly (e.g., <ul> with <li>).",
|
|
3122
3122
|
prompt: "State which child role(s) are required and suggest adding elements with those roles, or using equivalent native HTML elements.",
|
|
3123
|
-
run(
|
|
3124
|
-
var
|
|
3123
|
+
run(t) {
|
|
3124
|
+
var e;
|
|
3125
3125
|
const a = [];
|
|
3126
|
-
for (const i of
|
|
3126
|
+
for (const i of t.querySelectorAll("[role]")) {
|
|
3127
3127
|
if (g(i)) continue;
|
|
3128
|
-
const n = (
|
|
3129
|
-
if (!n || !(n in
|
|
3128
|
+
const n = (e = i.getAttribute("role")) == null ? void 0 : e.trim().toLowerCase();
|
|
3129
|
+
if (!n || !(n in ne) || i.getAttribute("aria-busy") === "true") continue;
|
|
3130
3130
|
if (n === "combobox") {
|
|
3131
3131
|
if (i.getAttribute("aria-expanded") !== "true") continue;
|
|
3132
3132
|
const l = i.tagName.toLowerCase();
|
|
3133
3133
|
if (l === "input" || l === "textarea") continue;
|
|
3134
3134
|
}
|
|
3135
|
-
const r =
|
|
3135
|
+
const r = ne[n], o = Ca(i, r);
|
|
3136
3136
|
if (o === "pass" || o === "empty" && Ea.has(n)) continue;
|
|
3137
3137
|
const s = r.map((l) => l.join(" or ")).join(", ");
|
|
3138
3138
|
a.push({
|
|
@@ -3152,16 +3152,16 @@ const La = {
|
|
|
3152
3152
|
description: "Certain ARIA roles must be contained within specific parent roles.",
|
|
3153
3153
|
guidance: "Some ARIA roles represent items that must exist within specific container roles. For example, a listitem must be within a list, a tab must be within a tablist. Wrap the element in the appropriate parent, or use native HTML elements that provide this structure (e.g., <li> inside <ul>).",
|
|
3154
3154
|
prompt: "State which parent role is required and suggest wrapping in an element with that role, or using equivalent native HTML structure.",
|
|
3155
|
-
run(
|
|
3156
|
-
var
|
|
3155
|
+
run(t) {
|
|
3156
|
+
var e;
|
|
3157
3157
|
const a = [];
|
|
3158
|
-
for (const i of
|
|
3158
|
+
for (const i of t.querySelectorAll("[role]")) {
|
|
3159
3159
|
if (g(i)) continue;
|
|
3160
|
-
const n = (
|
|
3161
|
-
if (!n || !(n in
|
|
3162
|
-
const r =
|
|
3160
|
+
const n = (e = i.getAttribute("role")) == null ? void 0 : e.trim().toLowerCase();
|
|
3161
|
+
if (!n || !(n in re)) continue;
|
|
3162
|
+
const r = re[n];
|
|
3163
3163
|
let o = i.parentElement, s = !1;
|
|
3164
|
-
for (; o && o !==
|
|
3164
|
+
for (; o && o !== t.documentElement; ) {
|
|
3165
3165
|
const l = q(o);
|
|
3166
3166
|
if (l && r.includes(l)) {
|
|
3167
3167
|
s = !0;
|
|
@@ -3179,7 +3179,7 @@ const La = {
|
|
|
3179
3179
|
}
|
|
3180
3180
|
return a;
|
|
3181
3181
|
}
|
|
3182
|
-
},
|
|
3182
|
+
}, oe = [
|
|
3183
3183
|
"a[href]",
|
|
3184
3184
|
"button:not([disabled])",
|
|
3185
3185
|
'input:not([disabled]):not([type="hidden"])',
|
|
@@ -3195,10 +3195,10 @@ const La = {
|
|
|
3195
3195
|
"embed",
|
|
3196
3196
|
"area[href]"
|
|
3197
3197
|
].join(", ");
|
|
3198
|
-
function Ra(
|
|
3199
|
-
let a =
|
|
3200
|
-
const
|
|
3201
|
-
for (; a && a !==
|
|
3198
|
+
function Ra(t) {
|
|
3199
|
+
let a = t;
|
|
3200
|
+
const e = t.ownerDocument, i = e.defaultView;
|
|
3201
|
+
for (; a && a !== e.body; ) {
|
|
3202
3202
|
if (a.style.display === "none" || a.style.visibility === "hidden") return !1;
|
|
3203
3203
|
if (i) {
|
|
3204
3204
|
const n = i.getComputedStyle(a);
|
|
@@ -3220,19 +3220,19 @@ const Na = {
|
|
|
3220
3220
|
guidance: "Setting aria-hidden='true' on the body element hides all page content from assistive technologies, making the page completely inaccessible to screen reader users. Remove aria-hidden from the body element. If you need to hide content temporarily (e.g., behind a modal), use aria-hidden on specific sections instead.",
|
|
3221
3221
|
prompt: "Instruct to remove aria-hidden='true' from the body element.",
|
|
3222
3222
|
skipAriaHidden: !1
|
|
3223
|
-
}, $a =
|
|
3223
|
+
}, $a = I(Na), Ma = {
|
|
3224
3224
|
id: "aria-hidden-focus",
|
|
3225
3225
|
wcag: ["4.1.2"],
|
|
3226
3226
|
level: "A",
|
|
3227
3227
|
description: "Elements with aria-hidden='true' must not contain focusable elements.",
|
|
3228
3228
|
guidance: "When aria-hidden='true' hides an element from assistive technologies but the element contains focusable children, keyboard users can focus those children but screen reader users won't know they exist. Either remove focusable elements from the hidden region, add tabindex='-1' to them, or remove aria-hidden.",
|
|
3229
3229
|
prompt: "This element can receive keyboard focus but is inside an aria-hidden region, making it invisible to screen readers. The context explains why it's focusable. Fix by either: (1) adding tabindex='-1' to remove it from tab order, (2) moving it outside the aria-hidden region, or (3) removing aria-hidden='true' from the ancestor if the content should be accessible.",
|
|
3230
|
-
run(
|
|
3230
|
+
run(t) {
|
|
3231
3231
|
const a = [];
|
|
3232
|
-
for (const
|
|
3233
|
-
if (
|
|
3234
|
-
const i = [...
|
|
3235
|
-
|
|
3232
|
+
for (const e of t.querySelectorAll('[aria-hidden="true"]')) {
|
|
3233
|
+
if (e === t.body) continue;
|
|
3234
|
+
const i = [...e.querySelectorAll(oe)];
|
|
3235
|
+
e.matches(oe) && i.push(e);
|
|
3236
3236
|
for (const n of i)
|
|
3237
3237
|
if (n instanceof HTMLElement) {
|
|
3238
3238
|
const r = n.getAttribute("tabindex");
|
|
@@ -3240,7 +3240,7 @@ const Na = {
|
|
|
3240
3240
|
const o = n.tagName.toLowerCase();
|
|
3241
3241
|
let s;
|
|
3242
3242
|
r !== null ? s = `has tabindex="${r}"` : o === "a" && n.hasAttribute("href") ? s = "is a link with href" : o === "button" ? s = "is a <button>" : o === "input" ? s = `is an <input type="${n.type}">` : o === "select" ? s = "is a <select>" : o === "textarea" ? s = "is a <textarea>" : o === "iframe" ? s = "is an <iframe>" : s = `is a natively focusable <${o}>`;
|
|
3243
|
-
const l = n ===
|
|
3243
|
+
const l = n === e ? n : n.closest('[aria-hidden="true"]');
|
|
3244
3244
|
a.push({
|
|
3245
3245
|
ruleId: "aria-hidden-focus",
|
|
3246
3246
|
selector: m(n),
|
|
@@ -3260,14 +3260,14 @@ const Na = {
|
|
|
3260
3260
|
description: "ARIA commands must have an accessible name.",
|
|
3261
3261
|
guidance: "Interactive ARIA command roles (button, link, menuitem) must have accessible names so users know what action they perform. Add visible text content, aria-label, or aria-labelledby to provide a name.",
|
|
3262
3262
|
prompt: "Based on the element's content or context, suggest an aria-label describing what this command does.",
|
|
3263
|
-
run(
|
|
3264
|
-
var
|
|
3263
|
+
run(t) {
|
|
3264
|
+
var e;
|
|
3265
3265
|
const a = [];
|
|
3266
|
-
for (const i of
|
|
3266
|
+
for (const i of t.querySelectorAll('[role="button"], [role="link"], [role="menuitem"]')) {
|
|
3267
3267
|
if (g(i) || T(i) || i.getRootNode() instanceof ShadowRoot || i.tagName.toLowerCase() === "button" || i.tagName.toLowerCase() === "a") continue;
|
|
3268
3268
|
if (!v(i)) {
|
|
3269
3269
|
const r = i.querySelector("img[alt]");
|
|
3270
|
-
if ((
|
|
3270
|
+
if ((e = r == null ? void 0 : r.getAttribute("alt")) != null && e.trim()) continue;
|
|
3271
3271
|
a.push({
|
|
3272
3272
|
ruleId: "aria-command-name",
|
|
3273
3273
|
selector: m(i),
|
|
@@ -3286,9 +3286,9 @@ const Na = {
|
|
|
3286
3286
|
description: "ARIA input fields must have an accessible name.",
|
|
3287
3287
|
guidance: "ARIA input widgets (combobox, listbox, searchbox, slider, spinbutton, textbox) must have accessible names so users understand what data to enter. Add a visible label with aria-labelledby, or use aria-label if a visible label is not possible.",
|
|
3288
3288
|
prompt: "Based on the context, suggest an aria-label describing what data this input field accepts.",
|
|
3289
|
-
run(
|
|
3290
|
-
const a = [],
|
|
3291
|
-
for (const i of
|
|
3289
|
+
run(t) {
|
|
3290
|
+
const a = [], e = '[role="combobox"], [role="listbox"], [role="searchbox"], [role="slider"], [role="spinbutton"], [role="textbox"]';
|
|
3291
|
+
for (const i of t.querySelectorAll(e)) {
|
|
3292
3292
|
if (g(i) || T(i) || i.getRootNode() instanceof ShadowRoot || i.matches("input, select, textarea")) continue;
|
|
3293
3293
|
v(i) || a.push({
|
|
3294
3294
|
ruleId: "aria-input-field-name",
|
|
@@ -3307,9 +3307,9 @@ const Na = {
|
|
|
3307
3307
|
description: "ARIA toggle fields must have an accessible name.",
|
|
3308
3308
|
guidance: "ARIA toggle controls (checkbox, switch, radio, menuitemcheckbox, menuitemradio) must have accessible names so users understand what option they're selecting. Add visible text content, aria-label, or use aria-labelledby to reference a visible label.",
|
|
3309
3309
|
prompt: "Based on the context, suggest an aria-label describing what option this toggle controls.",
|
|
3310
|
-
run(
|
|
3311
|
-
const a = [],
|
|
3312
|
-
for (const i of
|
|
3310
|
+
run(t) {
|
|
3311
|
+
const a = [], e = '[role="checkbox"], [role="switch"], [role="radio"], [role="menuitemcheckbox"], [role="menuitemradio"]';
|
|
3312
|
+
for (const i of t.querySelectorAll(e)) {
|
|
3313
3313
|
if (g(i) || T(i) || i.getRootNode() instanceof ShadowRoot || i.matches('input[type="checkbox"], input[type="radio"]')) continue;
|
|
3314
3314
|
v(i) || a.push({
|
|
3315
3315
|
ruleId: "aria-toggle-field-name",
|
|
@@ -3328,14 +3328,14 @@ const Na = {
|
|
|
3328
3328
|
description: "ARIA meter elements must have an accessible name.",
|
|
3329
3329
|
guidance: "Meter elements display a value within a known range (like disk usage or password strength). They must have accessible names so screen reader users understand what is being measured. Use aria-label or aria-labelledby to provide context.",
|
|
3330
3330
|
prompt: "Based on the context or value attributes, suggest an aria-label describing what this meter measures.",
|
|
3331
|
-
run(
|
|
3331
|
+
run(t) {
|
|
3332
3332
|
const a = [];
|
|
3333
|
-
for (const
|
|
3334
|
-
if (g(
|
|
3335
|
-
v(
|
|
3333
|
+
for (const e of t.querySelectorAll('[role="meter"], meter')) {
|
|
3334
|
+
if (g(e)) continue;
|
|
3335
|
+
v(e) || a.push({
|
|
3336
3336
|
ruleId: "aria-meter-name",
|
|
3337
|
-
selector: m(
|
|
3338
|
-
html: d(
|
|
3337
|
+
selector: m(e),
|
|
3338
|
+
html: d(e),
|
|
3339
3339
|
impact: "serious",
|
|
3340
3340
|
message: "Meter has no accessible name."
|
|
3341
3341
|
});
|
|
@@ -3349,14 +3349,14 @@ const Na = {
|
|
|
3349
3349
|
description: "ARIA progressbar elements must have an accessible name.",
|
|
3350
3350
|
guidance: "Progress indicators must have accessible names so screen reader users understand what process is being tracked. Use aria-label (e.g., 'File upload progress') or aria-labelledby to reference a visible heading or label.",
|
|
3351
3351
|
prompt: "Based on the context, suggest an aria-label describing what process this progressbar tracks.",
|
|
3352
|
-
run(
|
|
3352
|
+
run(t) {
|
|
3353
3353
|
const a = [];
|
|
3354
|
-
for (const
|
|
3355
|
-
if (g(
|
|
3356
|
-
v(
|
|
3354
|
+
for (const e of t.querySelectorAll('[role="progressbar"], progress')) {
|
|
3355
|
+
if (g(e)) continue;
|
|
3356
|
+
v(e) || a.push({
|
|
3357
3357
|
ruleId: "aria-progressbar-name",
|
|
3358
|
-
selector: m(
|
|
3359
|
-
html: d(
|
|
3358
|
+
selector: m(e),
|
|
3359
|
+
html: d(e),
|
|
3360
3360
|
impact: "serious",
|
|
3361
3361
|
message: "Progressbar has no accessible name."
|
|
3362
3362
|
});
|
|
@@ -3370,14 +3370,14 @@ const Na = {
|
|
|
3370
3370
|
description: "ARIA dialogs must have an accessible name.",
|
|
3371
3371
|
guidance: "Dialog and alertdialog elements must have accessible names so screen reader users understand the dialog's purpose when it opens. Use aria-label or aria-labelledby pointing to the dialog's heading. Native <dialog> elements should also have an accessible name.",
|
|
3372
3372
|
prompt: "Suggest adding aria-labelledby pointing to the dialog's heading element, or an aria-label describing the dialog's purpose.",
|
|
3373
|
-
run(
|
|
3373
|
+
run(t) {
|
|
3374
3374
|
const a = [];
|
|
3375
|
-
for (const
|
|
3376
|
-
if (g(
|
|
3377
|
-
v(
|
|
3375
|
+
for (const e of t.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')) {
|
|
3376
|
+
if (g(e)) continue;
|
|
3377
|
+
v(e) || a.push({
|
|
3378
3378
|
ruleId: "aria-dialog-name",
|
|
3379
|
-
selector: m(
|
|
3380
|
-
html: d(
|
|
3379
|
+
selector: m(e),
|
|
3380
|
+
html: d(e),
|
|
3381
3381
|
impact: "serious",
|
|
3382
3382
|
message: "Dialog has no accessible name."
|
|
3383
3383
|
});
|
|
@@ -3391,14 +3391,14 @@ const Na = {
|
|
|
3391
3391
|
description: "ARIA tooltips must have an accessible name.",
|
|
3392
3392
|
guidance: "Tooltip elements must have accessible names (usually their text content). The tooltip content itself typically serves as the accessible name. Ensure the tooltip contains descriptive text content or has aria-label.",
|
|
3393
3393
|
prompt: "Add text content to the tooltip describing the information it provides, or add aria-label.",
|
|
3394
|
-
run(
|
|
3394
|
+
run(t) {
|
|
3395
3395
|
const a = [];
|
|
3396
|
-
for (const
|
|
3397
|
-
if (g(
|
|
3398
|
-
v(
|
|
3396
|
+
for (const e of t.querySelectorAll('[role="tooltip"]')) {
|
|
3397
|
+
if (g(e)) continue;
|
|
3398
|
+
v(e) || a.push({
|
|
3399
3399
|
ruleId: "aria-tooltip-name",
|
|
3400
|
-
selector: m(
|
|
3401
|
-
html: d(
|
|
3400
|
+
selector: m(e),
|
|
3401
|
+
html: d(e),
|
|
3402
3402
|
impact: "serious",
|
|
3403
3403
|
message: "Tooltip has no accessible name."
|
|
3404
3404
|
});
|
|
@@ -3412,14 +3412,14 @@ const Na = {
|
|
|
3412
3412
|
description: "ARIA treeitem elements must have an accessible name.",
|
|
3413
3413
|
guidance: "Tree items must have accessible names so screen reader users can understand the tree structure and navigate it effectively. Provide text content, aria-label, or aria-labelledby for each treeitem.",
|
|
3414
3414
|
prompt: "Add text content describing this tree item, or add aria-label.",
|
|
3415
|
-
run(
|
|
3415
|
+
run(t) {
|
|
3416
3416
|
const a = [];
|
|
3417
|
-
for (const
|
|
3418
|
-
if (g(
|
|
3419
|
-
v(
|
|
3417
|
+
for (const e of t.querySelectorAll('[role="treeitem"]')) {
|
|
3418
|
+
if (g(e)) continue;
|
|
3419
|
+
v(e) || a.push({
|
|
3420
3420
|
ruleId: "aria-treeitem-name",
|
|
3421
|
-
selector: m(
|
|
3422
|
-
html: d(
|
|
3421
|
+
selector: m(e),
|
|
3422
|
+
html: d(e),
|
|
3423
3423
|
impact: "serious",
|
|
3424
3424
|
message: "Treeitem has no accessible name."
|
|
3425
3425
|
});
|
|
@@ -3433,8 +3433,8 @@ const Na = {
|
|
|
3433
3433
|
description: "ARIA attributes must not be prohibited for the element's role.",
|
|
3434
3434
|
guidance: "Some ARIA roles prohibit certain attributes. For example, roles like 'none', 'presentation', 'generic', and text-level roles (code, emphasis, strong) prohibit aria-label and aria-labelledby because naming is not supported for these roles. Remove the prohibited attributes or change the role.",
|
|
3435
3435
|
prompt: "Identify the prohibited attribute and recommend removing it from this element.",
|
|
3436
|
-
run(
|
|
3437
|
-
return
|
|
3436
|
+
run(t) {
|
|
3437
|
+
return G(t).prohibitedAttr;
|
|
3438
3438
|
}
|
|
3439
3439
|
}, Va = [
|
|
3440
3440
|
"a[href]",
|
|
@@ -3458,15 +3458,15 @@ const Na = {
|
|
|
3458
3458
|
"aria-owns",
|
|
3459
3459
|
"aria-relevant"
|
|
3460
3460
|
];
|
|
3461
|
-
function
|
|
3461
|
+
function se(t) {
|
|
3462
3462
|
const a = [];
|
|
3463
|
-
|
|
3464
|
-
for (const
|
|
3465
|
-
if (
|
|
3466
|
-
a.push(`has ${
|
|
3463
|
+
t.matches(Va) && a.push("element is focusable");
|
|
3464
|
+
for (const e of za)
|
|
3465
|
+
if (t.hasAttribute(e)) {
|
|
3466
|
+
a.push(`has ${e}`);
|
|
3467
3467
|
break;
|
|
3468
3468
|
}
|
|
3469
|
-
return (
|
|
3469
|
+
return (t.hasAttribute("aria-label") || t.hasAttribute("aria-labelledby")) && a.push("has accessible name"), a;
|
|
3470
3470
|
}
|
|
3471
3471
|
const Ua = {
|
|
3472
3472
|
id: "presentation-role-conflict",
|
|
@@ -3475,26 +3475,26 @@ const Ua = {
|
|
|
3475
3475
|
description: "Elements with role='presentation' or role='none' must not be focusable or have global ARIA attributes.",
|
|
3476
3476
|
guidance: "When an element has role='presentation' or role='none', it's marked as decorative and removed from the accessibility tree. However, if the element is focusable or has certain ARIA attributes, the presentation role is ignored and the element remains accessible. This creates confusion. Either remove the presentation role, or remove the focusability/ARIA attributes.",
|
|
3477
3477
|
prompt: "Identify the conflict (focusable or ARIA attribute) and suggest either removing the presentation role or removing the conflicting attribute/focusability.",
|
|
3478
|
-
run(
|
|
3478
|
+
run(t) {
|
|
3479
3479
|
const a = [];
|
|
3480
|
-
for (const
|
|
3481
|
-
if (g(
|
|
3482
|
-
const i =
|
|
3480
|
+
for (const e of t.querySelectorAll('[role="presentation"], [role="none"]')) {
|
|
3481
|
+
if (g(e)) continue;
|
|
3482
|
+
const i = se(e);
|
|
3483
3483
|
i.length > 0 && a.push({
|
|
3484
3484
|
ruleId: "presentation-role-conflict",
|
|
3485
|
-
selector: m(
|
|
3486
|
-
html: d(
|
|
3485
|
+
selector: m(e),
|
|
3486
|
+
html: d(e),
|
|
3487
3487
|
impact: "serious",
|
|
3488
3488
|
message: `Presentation role conflicts with: ${i.join(", ")}. The role will be ignored.`
|
|
3489
3489
|
});
|
|
3490
3490
|
}
|
|
3491
|
-
for (const
|
|
3492
|
-
if (g(
|
|
3493
|
-
const i =
|
|
3491
|
+
for (const e of t.querySelectorAll('img[alt=""]')) {
|
|
3492
|
+
if (g(e) || e.hasAttribute("role")) continue;
|
|
3493
|
+
const i = se(e);
|
|
3494
3494
|
i.length > 0 && a.push({
|
|
3495
3495
|
ruleId: "presentation-role-conflict",
|
|
3496
|
-
selector: m(
|
|
3497
|
-
html: d(
|
|
3496
|
+
selector: m(e),
|
|
3497
|
+
html: d(e),
|
|
3498
3498
|
impact: "serious",
|
|
3499
3499
|
message: `Element with implicit presentation role (alt="") conflicts with: ${i.join(", ")}. The decorative role will be ignored.`
|
|
3500
3500
|
});
|
|
@@ -3508,14 +3508,14 @@ const Ua = {
|
|
|
3508
3508
|
description: "<summary> elements must have an accessible name.",
|
|
3509
3509
|
guidance: "The <summary> element provides the visible label for a <details> disclosure widget. It must have descriptive text content so screen reader users understand what will be revealed when expanded. Add clear, concise text that indicates what content is contained in the details section.",
|
|
3510
3510
|
prompt: "Based on the surrounding context or details content, suggest text to add inside the <summary> element.",
|
|
3511
|
-
run(
|
|
3511
|
+
run(t) {
|
|
3512
3512
|
const a = [];
|
|
3513
|
-
for (const
|
|
3514
|
-
if (g(
|
|
3515
|
-
v(
|
|
3513
|
+
for (const e of t.querySelectorAll("details > summary:first-of-type")) {
|
|
3514
|
+
if (g(e)) continue;
|
|
3515
|
+
v(e) || a.push({
|
|
3516
3516
|
ruleId: "summary-name",
|
|
3517
|
-
selector: m(
|
|
3518
|
-
html: d(
|
|
3517
|
+
selector: m(e),
|
|
3518
|
+
html: d(e),
|
|
3519
3519
|
impact: "serious",
|
|
3520
3520
|
message: "<summary> element has no accessible name. Add descriptive text."
|
|
3521
3521
|
});
|
|
@@ -3523,11 +3523,11 @@ const Ua = {
|
|
|
3523
3523
|
return a;
|
|
3524
3524
|
}
|
|
3525
3525
|
};
|
|
3526
|
-
function Ya(
|
|
3526
|
+
function Ya(t) {
|
|
3527
3527
|
var n, r;
|
|
3528
|
-
const a = [],
|
|
3529
|
-
|
|
3530
|
-
const i =
|
|
3528
|
+
const a = [], e = t.getAttribute("href");
|
|
3529
|
+
e && a.push(`href: ${e}`);
|
|
3530
|
+
const i = t.parentElement;
|
|
3531
3531
|
if (i) {
|
|
3532
3532
|
const o = i.closest("h1, h2, h3, h4, h5, h6");
|
|
3533
3533
|
if ((n = o == null ? void 0 : o.textContent) != null && n.trim())
|
|
@@ -3547,17 +3547,17 @@ const Xa = {
|
|
|
3547
3547
|
description: "Links must have discernible text via content, aria-label, or aria-labelledby.",
|
|
3548
3548
|
guidance: "Screen reader users need to know where a link goes. Add descriptive text content, aria-label, or use aria-labelledby. For image links, ensure the image has alt text describing the link destination. Avoid generic text like 'click here' or 'read more'—link text should make sense out of context.",
|
|
3549
3549
|
prompt: "Based on the href or surrounding context, suggest descriptive link text or an aria-label.",
|
|
3550
|
-
run(
|
|
3550
|
+
run(t) {
|
|
3551
3551
|
const a = [];
|
|
3552
|
-
for (const
|
|
3553
|
-
if (g(
|
|
3554
|
-
v(
|
|
3552
|
+
for (const e of t.querySelectorAll('a[href], area[href], [role="link"]')) {
|
|
3553
|
+
if (g(e) || T(e) || e.getRootNode() instanceof ShadowRoot) continue;
|
|
3554
|
+
v(e) || a.push({
|
|
3555
3555
|
ruleId: "link-name",
|
|
3556
|
-
selector: m(
|
|
3557
|
-
html: d(
|
|
3556
|
+
selector: m(e),
|
|
3557
|
+
html: d(e),
|
|
3558
3558
|
impact: "serious",
|
|
3559
3559
|
message: "Link has no discernible text.",
|
|
3560
|
-
context: Ya(
|
|
3560
|
+
context: Ya(e)
|
|
3561
3561
|
});
|
|
3562
3562
|
}
|
|
3563
3563
|
return a;
|
|
@@ -3570,15 +3570,15 @@ const Xa = {
|
|
|
3570
3570
|
description: "Skip links must point to a valid target on the page.",
|
|
3571
3571
|
guidance: "Skip links allow keyboard users to bypass repetitive navigation and jump directly to main content. The skip link should be the first focusable element on the page, link to the main content (e.g., href='#main'), and become visible when focused. It can be visually hidden until focused using CSS.",
|
|
3572
3572
|
prompt: "A skip link is a single <a href='#main'>Skip to main content</a> as the first element in <body>. It can be visually hidden with CSS until focused. Explain this simple pattern.",
|
|
3573
|
-
run(
|
|
3574
|
-
const a = [],
|
|
3575
|
-
for (const i of
|
|
3573
|
+
run(t) {
|
|
3574
|
+
const a = [], e = t.querySelectorAll('a[href^="#"]');
|
|
3575
|
+
for (const i of e) {
|
|
3576
3576
|
const n = i.getAttribute("href");
|
|
3577
3577
|
if (!n || n === "#") continue;
|
|
3578
3578
|
const r = A(i).toLowerCase();
|
|
3579
3579
|
if (!(r.includes("skip") || r.includes("jump") || r.includes("main content") || r.includes("navigation"))) continue;
|
|
3580
3580
|
const s = n.slice(1);
|
|
3581
|
-
|
|
3581
|
+
t.getElementById(s) || a.push({
|
|
3582
3582
|
ruleId: "skip-link",
|
|
3583
3583
|
selector: m(i),
|
|
3584
3584
|
html: d(i),
|
|
@@ -3602,46 +3602,46 @@ const Xa = {
|
|
|
3602
3602
|
"inline-flex",
|
|
3603
3603
|
"inline-grid"
|
|
3604
3604
|
]);
|
|
3605
|
-
function Za(
|
|
3606
|
-
let a =
|
|
3605
|
+
function Za(t) {
|
|
3606
|
+
let a = t.parentElement;
|
|
3607
3607
|
for (; a; ) {
|
|
3608
|
-
const
|
|
3609
|
-
if (Ja.has(
|
|
3608
|
+
const e = w(a).display;
|
|
3609
|
+
if (Ja.has(e))
|
|
3610
3610
|
return ei(a) ? a : null;
|
|
3611
3611
|
a = a.parentElement;
|
|
3612
3612
|
}
|
|
3613
3613
|
return null;
|
|
3614
3614
|
}
|
|
3615
|
-
function ei(
|
|
3616
|
-
const a =
|
|
3617
|
-
|
|
3615
|
+
function ei(t) {
|
|
3616
|
+
const a = t.ownerDocument.createTreeWalker(
|
|
3617
|
+
t,
|
|
3618
3618
|
NodeFilter.SHOW_TEXT
|
|
3619
3619
|
);
|
|
3620
|
-
let
|
|
3620
|
+
let e = "", i;
|
|
3621
3621
|
for (; i = a.nextNode(); ) {
|
|
3622
3622
|
if (!i.data.trim()) continue;
|
|
3623
3623
|
let n = i.parentElement, r = !1;
|
|
3624
|
-
for (; n && n !==
|
|
3624
|
+
for (; n && n !== t; ) {
|
|
3625
3625
|
if (n.tagName === "A") {
|
|
3626
3626
|
r = !0;
|
|
3627
3627
|
break;
|
|
3628
3628
|
}
|
|
3629
3629
|
n = n.parentElement;
|
|
3630
3630
|
}
|
|
3631
|
-
r || (
|
|
3631
|
+
r || (e += i.data);
|
|
3632
3632
|
}
|
|
3633
|
-
return new RegExp("\\p{L}{2,}", "u").test(
|
|
3633
|
+
return new RegExp("\\p{L}{2,}", "u").test(e);
|
|
3634
3634
|
}
|
|
3635
|
-
function ti(
|
|
3636
|
-
const
|
|
3637
|
-
|
|
3635
|
+
function ti(t, a) {
|
|
3636
|
+
const e = t.ownerDocument.createTreeWalker(
|
|
3637
|
+
t,
|
|
3638
3638
|
NodeFilter.SHOW_TEXT
|
|
3639
3639
|
);
|
|
3640
3640
|
let i;
|
|
3641
|
-
for (; i =
|
|
3641
|
+
for (; i = e.nextNode(); ) {
|
|
3642
3642
|
if (!i.data.trim()) continue;
|
|
3643
3643
|
let n = i.parentElement, r = !1, o = n;
|
|
3644
|
-
for (; o && o !==
|
|
3644
|
+
for (; o && o !== t; ) {
|
|
3645
3645
|
if (o.tagName === "A") {
|
|
3646
3646
|
r = !0;
|
|
3647
3647
|
break;
|
|
@@ -3649,34 +3649,41 @@ function ti(e, a) {
|
|
|
3649
3649
|
o = o.parentElement;
|
|
3650
3650
|
}
|
|
3651
3651
|
if (!r && n)
|
|
3652
|
-
return N(
|
|
3652
|
+
return N(w(n).color);
|
|
3653
3653
|
}
|
|
3654
3654
|
return null;
|
|
3655
3655
|
}
|
|
3656
|
-
function ai(
|
|
3657
|
-
const
|
|
3658
|
-
if ((
|
|
3656
|
+
function ai(t, a, e) {
|
|
3657
|
+
const i = e.textDecorationLine || e.textDecoration || "", n = a.textDecorationLine || a.textDecoration || "";
|
|
3658
|
+
if ((n.includes("underline") || n.includes("line-through")) && n !== i)
|
|
3659
3659
|
return !0;
|
|
3660
|
-
const
|
|
3661
|
-
if (
|
|
3660
|
+
const r = parseFloat(a.borderBottomWidth) || 0, o = a.borderBottomStyle || "";
|
|
3661
|
+
if (r > 0 && o !== "none" && o !== "hidden")
|
|
3662
3662
|
return !0;
|
|
3663
|
-
const
|
|
3664
|
-
if (
|
|
3663
|
+
const s = parseFloat(a.outlineWidth) || 0, l = a.outlineStyle || "";
|
|
3664
|
+
if (s > 0 && l !== "none")
|
|
3665
3665
|
return !0;
|
|
3666
|
-
const
|
|
3667
|
-
if (
|
|
3666
|
+
const h = a.backgroundImage || "";
|
|
3667
|
+
if (h && h !== "none" && h !== "initial")
|
|
3668
3668
|
return !0;
|
|
3669
|
-
const
|
|
3670
|
-
if (Math.abs(
|
|
3669
|
+
const c = F(e.fontWeight), u = F(a.fontWeight);
|
|
3670
|
+
if (Math.abs(u - c) >= 300 || a.fontStyle !== e.fontStyle)
|
|
3671
3671
|
return !0;
|
|
3672
|
-
const
|
|
3673
|
-
|
|
3672
|
+
const p = parseFloat(a.fontSize) || 16, b = parseFloat(e.fontSize) || 16;
|
|
3673
|
+
if (b > 0 && p / b >= 1.2)
|
|
3674
|
+
return !0;
|
|
3675
|
+
for (const f of t.querySelectorAll("*")) {
|
|
3676
|
+
const y = w(f), x = y.textDecorationLine || y.textDecoration || "";
|
|
3677
|
+
if ((x.includes("underline") || x.includes("line-through")) && x !== i || Math.abs(F(y.fontWeight) - c) >= 300)
|
|
3678
|
+
return !0;
|
|
3679
|
+
}
|
|
3680
|
+
return !1;
|
|
3674
3681
|
}
|
|
3675
|
-
function
|
|
3676
|
-
return
|
|
3682
|
+
function F(t) {
|
|
3683
|
+
return t === "bold" ? 700 : t === "normal" ? 400 : parseInt(t) || 400;
|
|
3677
3684
|
}
|
|
3678
|
-
function le(
|
|
3679
|
-
return "#" + [
|
|
3685
|
+
function le(t, a, e) {
|
|
3686
|
+
return "#" + [t, a, e].map((i) => i.toString(16).padStart(2, "0")).join("");
|
|
3680
3687
|
}
|
|
3681
3688
|
const ii = {
|
|
3682
3689
|
id: "link-in-text-block",
|
|
@@ -3685,27 +3692,27 @@ const ii = {
|
|
|
3685
3692
|
description: "Links within text blocks must be distinguishable by more than color alone.",
|
|
3686
3693
|
guidance: "Users who cannot perceive color differences need other visual cues to identify links. Links in text should have underlines or other non-color indicators. If using color alone, ensure 3:1 contrast with surrounding text AND provide additional indication on focus/hover.",
|
|
3687
3694
|
prompt: "Explain how to make this link visually distinguishable without relying on color alone.",
|
|
3688
|
-
run(
|
|
3695
|
+
run(t) {
|
|
3689
3696
|
const a = [];
|
|
3690
|
-
for (const
|
|
3691
|
-
if (g(
|
|
3692
|
-
const i =
|
|
3697
|
+
for (const e of t.querySelectorAll("a[href]")) {
|
|
3698
|
+
if (g(e) || !A(e).trim() || e.closest('nav, header, footer, [role="navigation"], [role="banner"], [role="contentinfo"]')) continue;
|
|
3699
|
+
const i = w(e), n = i.display || "inline";
|
|
3693
3700
|
if (!Qa.has(n)) continue;
|
|
3694
|
-
const r = Za(
|
|
3701
|
+
const r = Za(e);
|
|
3695
3702
|
if (!r) continue;
|
|
3696
|
-
const o =
|
|
3697
|
-
if (ai(i, o)) continue;
|
|
3703
|
+
const o = w(r);
|
|
3704
|
+
if (ai(e, i, o)) continue;
|
|
3698
3705
|
const s = N(i.color), l = ti(r);
|
|
3699
3706
|
if (!s || !l) continue;
|
|
3700
|
-
const
|
|
3701
|
-
if (u >= 3) continue;
|
|
3702
|
-
const
|
|
3707
|
+
const h = R(...s), c = R(...l), u = ge(h, c);
|
|
3708
|
+
if (u < 1.1 || u >= 3) continue;
|
|
3709
|
+
const p = le(...s), b = le(...l), f = `link color: ${p} rgb(${s.join(", ")}), surrounding text: ${b} rgb(${l.join(", ")}), ratio: ${u.toFixed(2)}:1`;
|
|
3703
3710
|
a.push({
|
|
3704
3711
|
ruleId: "link-in-text-block",
|
|
3705
|
-
selector: m(
|
|
3706
|
-
html: d(
|
|
3712
|
+
selector: m(e),
|
|
3713
|
+
html: d(e),
|
|
3707
3714
|
impact: "serious",
|
|
3708
|
-
message: "Link in text block is not visually distinguishable from surrounding text. Add
|
|
3715
|
+
message: "Link in text block is not visually distinguishable from surrounding text. Add a non-color visual indicator such as an underline or border.",
|
|
3709
3716
|
context: f
|
|
3710
3717
|
});
|
|
3711
3718
|
}
|
|
@@ -3718,20 +3725,20 @@ const ii = {
|
|
|
3718
3725
|
description: "The <html> element must have a lang attribute.",
|
|
3719
3726
|
guidance: "Screen readers use the lang attribute to determine which language rules and pronunciation to use. Without it, content may be mispronounced. Set lang to the primary language of the page (e.g., lang='en' for English, lang='es' for Spanish).",
|
|
3720
3727
|
prompt: `The page is missing a lang attribute on <html>. Use the text sample in context to determine the primary language and suggest the correct BCP 47 code (e.g. 'en' for English, 'es' for Spanish, 'fr' for French, 'de' for German, 'ja' for Japanese, 'zh' for Chinese, 'pt' for Portuguese, 'ar' for Arabic). Add lang to the <html> element: <html lang="...">.`,
|
|
3721
|
-
run(
|
|
3722
|
-
var
|
|
3723
|
-
const a =
|
|
3728
|
+
run(t) {
|
|
3729
|
+
var e, i;
|
|
3730
|
+
const a = t.documentElement;
|
|
3724
3731
|
if (a.tagName.toLowerCase() !== "html") return [];
|
|
3725
|
-
if (!
|
|
3726
|
-
const n =
|
|
3732
|
+
if (!t.doctype && t.body) {
|
|
3733
|
+
const n = t.body.children;
|
|
3727
3734
|
if (n.length > 0 && Array.from(n).every(
|
|
3728
3735
|
(r) => r.tagName.toLowerCase() === "svg" || r.tagName.toLowerCase() === "math"
|
|
3729
3736
|
)) return [];
|
|
3730
3737
|
}
|
|
3731
|
-
if (!((
|
|
3738
|
+
if (!((e = a.getAttribute("lang")) != null && e.trim())) {
|
|
3732
3739
|
let n;
|
|
3733
|
-
if (
|
|
3734
|
-
const r = ((i =
|
|
3740
|
+
if (t.body) {
|
|
3741
|
+
const r = ((i = t.body.textContent) == null ? void 0 : i.trim().replace(/\s+/g, " ")) || "";
|
|
3735
3742
|
r && (n = r.slice(0, 200));
|
|
3736
3743
|
}
|
|
3737
3744
|
return [{
|
|
@@ -3750,9 +3757,9 @@ const ii = {
|
|
|
3750
3757
|
), oi = new Set(
|
|
3751
3758
|
"aar abk afr aka amh ara arg asm ava ave aym aze bak bam bel ben bih bis bod bos bre bul cat ces cha che chu chv cor cos cre cym dan deu div dzo ell eng epo est eus ewe fao fas fij fin fra fry ful gla gle glg glv grn guj hat hau hbs heb her hin hmo hrv hun hye ibo iii iku ile ina ind ipk isl ita jav jpn kal kan kas kat kau kaz khm kik kin kir kom kon kor kua kur lao lat lav lim lin lit ltz lub lug mah mal mar mkd mlg mlt mon mri msa mya nau nav nbl nde ndo nep nld nno nob nor nya oci oji ori orm oss pan pli pol por pus que roh ron run rus sag san sin slk slv sme smo sna snd som sot spa sqi srd srp ssw sun swa swe tah tam tat tel tgk tgl tha tir ton tsn tso tuk tur twi uig ukr urd uzb ven vie vol wln wol xho yid yor zha zho zul".split(" ")
|
|
3752
3759
|
), si = /^[a-z]{2,8}(-[a-z0-9]{1,8})*$/i;
|
|
3753
|
-
function fe(
|
|
3754
|
-
if (!si.test(
|
|
3755
|
-
const a =
|
|
3760
|
+
function fe(t) {
|
|
3761
|
+
if (!si.test(t)) return !1;
|
|
3762
|
+
const a = t.split("-")[0].toLowerCase();
|
|
3756
3763
|
return a.length === 2 ? ri.has(a) : a.length === 3 ? !oi.has(a) : !1;
|
|
3757
3764
|
}
|
|
3758
3765
|
const li = {
|
|
@@ -3762,28 +3769,28 @@ const li = {
|
|
|
3762
3769
|
description: "The lang attribute on <html> must have a valid value.",
|
|
3763
3770
|
guidance: "The lang attribute must use a valid BCP 47 language tag. Use a 2 or 3 letter language code (e.g., 'en', 'fr', 'zh'), optionally followed by a region code (e.g., 'en-US', 'pt-BR'). Invalid tags prevent screen readers from correctly pronouncing content.",
|
|
3764
3771
|
prompt: "Suggest the correct BCP 47 language tag based on the invalid value provided.",
|
|
3765
|
-
run(
|
|
3766
|
-
var
|
|
3767
|
-
const a = (
|
|
3772
|
+
run(t) {
|
|
3773
|
+
var e;
|
|
3774
|
+
const a = (e = t.documentElement.getAttribute("lang")) == null ? void 0 : e.trim();
|
|
3768
3775
|
return a && !fe(a) ? [{
|
|
3769
3776
|
ruleId: "html-lang-valid",
|
|
3770
3777
|
selector: "html",
|
|
3771
|
-
html: d(
|
|
3778
|
+
html: d(t.documentElement),
|
|
3772
3779
|
impact: "serious",
|
|
3773
3780
|
message: `Invalid lang attribute value "${a}".`
|
|
3774
3781
|
}] : [];
|
|
3775
3782
|
}
|
|
3776
3783
|
};
|
|
3777
|
-
function ce(
|
|
3784
|
+
function ce(t) {
|
|
3778
3785
|
var i;
|
|
3779
|
-
const a =
|
|
3780
|
-
let
|
|
3781
|
-
for (;
|
|
3782
|
-
if (!
|
|
3783
|
-
const n =
|
|
3786
|
+
const a = t.ownerDocument.createTreeWalker(t, NodeFilter.SHOW_TEXT);
|
|
3787
|
+
let e;
|
|
3788
|
+
for (; e = a.nextNode(); ) {
|
|
3789
|
+
if (!e.data.trim()) continue;
|
|
3790
|
+
const n = e.parentElement;
|
|
3784
3791
|
if (!n || n instanceof HTMLElement && (n.hidden || n.style.display === "none")) continue;
|
|
3785
3792
|
let r = n, o = !1;
|
|
3786
|
-
for (; r && r !==
|
|
3793
|
+
for (; r && r !== t; ) {
|
|
3787
3794
|
if (r.hasAttribute("lang")) {
|
|
3788
3795
|
o = !0;
|
|
3789
3796
|
break;
|
|
@@ -3792,10 +3799,10 @@ function ce(e) {
|
|
|
3792
3799
|
}
|
|
3793
3800
|
if (!o) return !0;
|
|
3794
3801
|
}
|
|
3795
|
-
for (const n of
|
|
3802
|
+
for (const n of t.querySelectorAll("img[alt]")) {
|
|
3796
3803
|
if (!((i = n.getAttribute("alt")) == null ? void 0 : i.trim())) continue;
|
|
3797
3804
|
let o = n.parentElement, s = !1;
|
|
3798
|
-
for (; o && o !==
|
|
3805
|
+
for (; o && o !== t; ) {
|
|
3799
3806
|
if (o.hasAttribute("lang")) {
|
|
3800
3807
|
s = !0;
|
|
3801
3808
|
break;
|
|
@@ -3813,25 +3820,25 @@ const ci = {
|
|
|
3813
3820
|
description: "The lang attribute must have a valid value on all elements.",
|
|
3814
3821
|
guidance: "When content in a different language appears within a page (e.g., a French quote in an English document), wrap it with a lang attribute to ensure correct pronunciation. The lang value must be a valid BCP 47 tag. Common codes: en, es, fr, de, zh, ja, pt, ar, ru.",
|
|
3815
3822
|
prompt: "Identify the content's language and suggest the correct BCP 47 tag.",
|
|
3816
|
-
run(
|
|
3823
|
+
run(t) {
|
|
3817
3824
|
const a = [];
|
|
3818
|
-
for (const
|
|
3819
|
-
if (g(
|
|
3820
|
-
const i =
|
|
3825
|
+
for (const e of t.querySelectorAll("[lang]")) {
|
|
3826
|
+
if (g(e) || e === t.documentElement) continue;
|
|
3827
|
+
const i = e.getAttribute("lang"), n = i == null ? void 0 : i.trim();
|
|
3821
3828
|
if (i && !n) {
|
|
3822
|
-
ce(
|
|
3829
|
+
ce(e) && a.push({
|
|
3823
3830
|
ruleId: "valid-lang",
|
|
3824
|
-
selector: m(
|
|
3825
|
-
html: d(
|
|
3831
|
+
selector: m(e),
|
|
3832
|
+
html: d(e),
|
|
3826
3833
|
impact: "serious",
|
|
3827
3834
|
message: "Empty lang attribute value."
|
|
3828
3835
|
});
|
|
3829
3836
|
continue;
|
|
3830
3837
|
}
|
|
3831
|
-
n && ce(
|
|
3838
|
+
n && ce(e) && (fe(n) || a.push({
|
|
3832
3839
|
ruleId: "valid-lang",
|
|
3833
|
-
selector: m(
|
|
3834
|
-
html: d(
|
|
3840
|
+
selector: m(e),
|
|
3841
|
+
html: d(e),
|
|
3835
3842
|
impact: "serious",
|
|
3836
3843
|
message: `Invalid lang attribute value "${n}".`
|
|
3837
3844
|
}));
|
|
@@ -3845,18 +3852,18 @@ const ci = {
|
|
|
3845
3852
|
description: "The lang and xml:lang attributes on <html> must match.",
|
|
3846
3853
|
guidance: "In XHTML documents, if both lang and xml:lang are present, they must specify the same base language. Mismatched values confuse assistive technologies. Either remove xml:lang (preferred for HTML5) or ensure both attributes have identical values.",
|
|
3847
3854
|
prompt: "Explain whether to remove xml:lang or align it with the lang value.",
|
|
3848
|
-
run(
|
|
3855
|
+
run(t) {
|
|
3849
3856
|
var n, r;
|
|
3850
|
-
const a =
|
|
3851
|
-
if (
|
|
3852
|
-
const o =
|
|
3857
|
+
const a = t.documentElement, e = (n = a.getAttribute("lang")) == null ? void 0 : n.trim().toLowerCase(), i = (r = a.getAttribute("xml:lang")) == null ? void 0 : r.trim().toLowerCase();
|
|
3858
|
+
if (e && i) {
|
|
3859
|
+
const o = e.split("-")[0], s = i.split("-")[0];
|
|
3853
3860
|
if (o !== s)
|
|
3854
3861
|
return [{
|
|
3855
3862
|
ruleId: "html-xml-lang-mismatch",
|
|
3856
3863
|
selector: "html",
|
|
3857
3864
|
html: d(a),
|
|
3858
3865
|
impact: "moderate",
|
|
3859
|
-
message: `lang="${
|
|
3866
|
+
message: `lang="${e}" and xml:lang="${i}" do not match.`
|
|
3860
3867
|
}];
|
|
3861
3868
|
}
|
|
3862
3869
|
return [];
|
|
@@ -3868,19 +3875,19 @@ const ci = {
|
|
|
3868
3875
|
description: "All cells in a table using headers attribute must reference valid header IDs.",
|
|
3869
3876
|
guidance: "The headers attribute on table cells must reference IDs of header cells (th or td) within the same table. This creates explicit associations for screen readers. Verify all referenced IDs exist and spell them correctly. For simple tables, consider using scope on th elements instead.",
|
|
3870
3877
|
prompt: "Identify the invalid header ID reference and suggest the correct ID or how to fix it.",
|
|
3871
|
-
run(
|
|
3878
|
+
run(t) {
|
|
3872
3879
|
const a = [];
|
|
3873
|
-
for (const
|
|
3874
|
-
if (g(
|
|
3875
|
-
const i =
|
|
3880
|
+
for (const e of t.querySelectorAll("td[headers]")) {
|
|
3881
|
+
if (g(e)) continue;
|
|
3882
|
+
const i = e.closest("table");
|
|
3876
3883
|
if (!i) continue;
|
|
3877
|
-
const n =
|
|
3884
|
+
const n = e.getAttribute("id"), r = e.getAttribute("headers").split(/\s+/);
|
|
3878
3885
|
for (const o of r) {
|
|
3879
3886
|
if (o === n) {
|
|
3880
3887
|
a.push({
|
|
3881
3888
|
ruleId: "td-headers-attr",
|
|
3882
|
-
selector: m(
|
|
3883
|
-
html: d(
|
|
3889
|
+
selector: m(e),
|
|
3890
|
+
html: d(e),
|
|
3884
3891
|
impact: "serious",
|
|
3885
3892
|
message: `Headers attribute references the cell itself ("${o}").`
|
|
3886
3893
|
});
|
|
@@ -3889,8 +3896,8 @@ const ci = {
|
|
|
3889
3896
|
if (!i.querySelector(`th#${CSS.escape(o)}, td#${CSS.escape(o)}`)) {
|
|
3890
3897
|
a.push({
|
|
3891
3898
|
ruleId: "td-headers-attr",
|
|
3892
|
-
selector: m(
|
|
3893
|
-
html: d(
|
|
3899
|
+
selector: m(e),
|
|
3900
|
+
html: d(e),
|
|
3894
3901
|
impact: "serious",
|
|
3895
3902
|
message: `Headers attribute references non-existent ID "${o}".`
|
|
3896
3903
|
});
|
|
@@ -3907,15 +3914,15 @@ const ci = {
|
|
|
3907
3914
|
description: "Table headers should be associated with data cells.",
|
|
3908
3915
|
guidance: "A table with header cells (th) but no data cells (td) is likely a misuse of table markup for layout or has missing content. Either add data cells that the headers describe, or use appropriate non-table markup if this is not tabular data.",
|
|
3909
3916
|
prompt: "Explain whether this table needs data cells or if non-table layout would be more appropriate.",
|
|
3910
|
-
run(
|
|
3917
|
+
run(t) {
|
|
3911
3918
|
const a = [];
|
|
3912
|
-
for (const
|
|
3913
|
-
if (g(
|
|
3914
|
-
const i =
|
|
3919
|
+
for (const e of t.querySelectorAll("table")) {
|
|
3920
|
+
if (g(e) || e.getAttribute("role") === "presentation" || e.getAttribute("role") === "none") continue;
|
|
3921
|
+
const i = e.querySelectorAll("th"), n = e.querySelectorAll("td");
|
|
3915
3922
|
i.length > 0 && n.length === 0 && a.push({
|
|
3916
3923
|
ruleId: "th-has-data-cells",
|
|
3917
|
-
selector: m(
|
|
3918
|
-
html: d(
|
|
3924
|
+
selector: m(e),
|
|
3925
|
+
html: d(e),
|
|
3919
3926
|
impact: "serious",
|
|
3920
3927
|
message: "Table has header cells but no data cells."
|
|
3921
3928
|
});
|
|
@@ -3929,39 +3936,39 @@ const ci = {
|
|
|
3929
3936
|
description: "Data cells in tables larger than 3x3 should have associated headers.",
|
|
3930
3937
|
guidance: "In complex tables, screen reader users need header associations to understand data cells. Use th elements with scope attribute, or the headers attribute on td elements. For simple tables (≤3x3), this is less critical as context is usually clear.",
|
|
3931
3938
|
prompt: "Explain whether to use scope attributes on headers or headers attribute on this cell.",
|
|
3932
|
-
run(
|
|
3933
|
-
var
|
|
3939
|
+
run(t) {
|
|
3940
|
+
var e, i;
|
|
3934
3941
|
const a = [];
|
|
3935
|
-
for (const n of
|
|
3942
|
+
for (const n of t.querySelectorAll("table")) {
|
|
3936
3943
|
if (g(n) || n.getAttribute("role") === "presentation" || n.getAttribute("role") === "none") continue;
|
|
3937
3944
|
const r = n.querySelectorAll("tr"), o = r.length;
|
|
3938
3945
|
let s = 0;
|
|
3939
3946
|
for (const u of r) {
|
|
3940
|
-
const
|
|
3947
|
+
const p = u.querySelectorAll("td, th");
|
|
3941
3948
|
let b = 0;
|
|
3942
|
-
for (const f of
|
|
3949
|
+
for (const f of p)
|
|
3943
3950
|
b += parseInt(f.getAttribute("colspan") || "1", 10);
|
|
3944
3951
|
s = Math.max(s, b);
|
|
3945
3952
|
}
|
|
3946
3953
|
if (o <= 3 && s <= 3) continue;
|
|
3947
|
-
const l = n.querySelector("th") !== null,
|
|
3954
|
+
const l = n.querySelector("th") !== null, h = n.querySelector("th[scope]") !== null, c = n.querySelector("td[headers]") !== null;
|
|
3948
3955
|
if (l)
|
|
3949
3956
|
for (const u of n.querySelectorAll("td")) {
|
|
3950
3957
|
if (g(u) || u.hasAttribute("headers")) continue;
|
|
3951
|
-
const
|
|
3952
|
-
if (!
|
|
3953
|
-
const b =
|
|
3954
|
-
let
|
|
3955
|
-
const
|
|
3956
|
-
if (
|
|
3957
|
-
const
|
|
3958
|
-
|
|
3958
|
+
const p = u.closest("tr");
|
|
3959
|
+
if (!p) continue;
|
|
3960
|
+
const b = p.querySelector("th") !== null, f = Array.from(p.children).indexOf(u);
|
|
3961
|
+
let y = !1;
|
|
3962
|
+
const x = n.querySelector("thead");
|
|
3963
|
+
if (x) {
|
|
3964
|
+
const S = x.querySelector("tr");
|
|
3965
|
+
S && ((e = S.querySelectorAll("th, td")[f]) == null ? void 0 : e.tagName.toLowerCase()) === "th" && (y = !0);
|
|
3959
3966
|
}
|
|
3960
|
-
if (!
|
|
3961
|
-
const
|
|
3962
|
-
|
|
3967
|
+
if (!y) {
|
|
3968
|
+
const S = n.querySelector("tbody > tr, tr");
|
|
3969
|
+
S && ((i = S.querySelectorAll("th, td")[f]) == null ? void 0 : i.tagName.toLowerCase()) === "th" && (y = !0);
|
|
3963
3970
|
}
|
|
3964
|
-
if (!b && !
|
|
3971
|
+
if (!b && !y && !h && !c) {
|
|
3965
3972
|
a.push({
|
|
3966
3973
|
ruleId: "td-has-header",
|
|
3967
3974
|
selector: m(u),
|
|
@@ -3982,13 +3989,13 @@ const ci = {
|
|
|
3982
3989
|
description: "The scope attribute on table headers must have a valid value.",
|
|
3983
3990
|
guidance: "The scope attribute tells screen readers which cells a header applies to. Valid values are: row, col, rowgroup, colgroup. Using invalid values breaks the association between headers and cells.",
|
|
3984
3991
|
prompt: "Explain which scope value (row, col, rowgroup, colgroup) is appropriate for this header.",
|
|
3985
|
-
run(
|
|
3992
|
+
run(t) {
|
|
3986
3993
|
var i;
|
|
3987
|
-
const a = [],
|
|
3988
|
-
for (const n of
|
|
3994
|
+
const a = [], e = /* @__PURE__ */ new Set(["row", "col", "rowgroup", "colgroup"]);
|
|
3995
|
+
for (const n of t.querySelectorAll("th[scope]")) {
|
|
3989
3996
|
if (g(n)) continue;
|
|
3990
3997
|
const r = (i = n.getAttribute("scope")) == null ? void 0 : i.toLowerCase();
|
|
3991
|
-
r && !
|
|
3998
|
+
r && !e.has(r) && a.push({
|
|
3992
3999
|
ruleId: "scope-attr-valid",
|
|
3993
4000
|
selector: m(n),
|
|
3994
4001
|
html: d(n),
|
|
@@ -4006,65 +4013,65 @@ const ci = {
|
|
|
4006
4013
|
description: "Table header cells should have visible text.",
|
|
4007
4014
|
guidance: "Empty table headers provide no information to screen reader users. Either add descriptive text to the header, or if the header is intentionally empty (like a corner cell), consider using a td element instead or adding a visually hidden label.",
|
|
4008
4015
|
prompt: "Suggest header text based on the column/row content, or explain if this should be a td instead.",
|
|
4009
|
-
run(
|
|
4016
|
+
run(t) {
|
|
4010
4017
|
const a = [];
|
|
4011
|
-
for (const
|
|
4012
|
-
if (g(
|
|
4013
|
-
const i =
|
|
4014
|
-
(i == null ? void 0 : i.getAttribute("role")) === "presentation" || (i == null ? void 0 : i.getAttribute("role")) === "none" || v(
|
|
4018
|
+
for (const e of t.querySelectorAll("th")) {
|
|
4019
|
+
if (g(e)) continue;
|
|
4020
|
+
const i = e.closest("table");
|
|
4021
|
+
(i == null ? void 0 : i.getAttribute("role")) === "presentation" || (i == null ? void 0 : i.getAttribute("role")) === "none" || v(e) || a.push({
|
|
4015
4022
|
ruleId: "empty-table-header",
|
|
4016
|
-
selector: m(
|
|
4017
|
-
html: d(
|
|
4023
|
+
selector: m(e),
|
|
4024
|
+
html: d(e),
|
|
4018
4025
|
impact: "minor",
|
|
4019
4026
|
message: "Table header cell is empty. Add text or use aria-label."
|
|
4020
4027
|
});
|
|
4021
4028
|
}
|
|
4022
4029
|
return a;
|
|
4023
4030
|
}
|
|
4024
|
-
},
|
|
4031
|
+
}, W = ["aria-labelledby", "aria-describedby", "aria-controls", "aria-owns", "aria-flowto"], bi = {
|
|
4025
4032
|
id: "duplicate-id-aria",
|
|
4026
4033
|
wcag: ["4.1.2"],
|
|
4027
4034
|
level: "A",
|
|
4028
4035
|
description: "IDs used in ARIA and label associations must be unique to avoid broken references.",
|
|
4029
4036
|
guidance: "When aria-labelledby, aria-describedby, aria-controls, or label[for] reference a duplicate ID, only the first matching element is used. This breaks the intended relationship and may leave controls unnamed or descriptions missing. Ensure IDs referenced by ARIA attributes and label associations are unique throughout the document.",
|
|
4030
4037
|
prompt: "Identify which attribute references this ID and suggest a unique replacement.",
|
|
4031
|
-
run(
|
|
4032
|
-
const a = [],
|
|
4033
|
-
for (const n of
|
|
4034
|
-
for (const r of
|
|
4038
|
+
run(t) {
|
|
4039
|
+
const a = [], e = /* @__PURE__ */ new Set();
|
|
4040
|
+
for (const n of t.querySelectorAll("[aria-labelledby], [aria-describedby], [aria-controls], [aria-owns], [aria-flowto]"))
|
|
4041
|
+
for (const r of W) {
|
|
4035
4042
|
const o = n.getAttribute(r);
|
|
4036
|
-
o && o.split(/\s+/).forEach((s) =>
|
|
4043
|
+
o && o.split(/\s+/).forEach((s) => e.add(s));
|
|
4037
4044
|
}
|
|
4038
|
-
for (const n of
|
|
4045
|
+
for (const n of t.querySelectorAll("label[for]")) {
|
|
4039
4046
|
const r = n.getAttribute("for");
|
|
4040
|
-
r &&
|
|
4047
|
+
r && e.add(r);
|
|
4041
4048
|
}
|
|
4042
4049
|
const i = /* @__PURE__ */ new Map();
|
|
4043
|
-
for (const n of
|
|
4044
|
-
|
|
4050
|
+
for (const n of t.querySelectorAll("[id]"))
|
|
4051
|
+
e.has(n.id) && (n instanceof HTMLElement && (n.style.display === "none" || n.style.visibility === "hidden" || n.hidden) || i.set(n.id, (i.get(n.id) ?? 0) + 1));
|
|
4045
4052
|
for (const [n, r] of i) {
|
|
4046
4053
|
if (r <= 1) continue;
|
|
4047
|
-
const o =
|
|
4048
|
-
|
|
4049
|
-
), l =
|
|
4050
|
-
let
|
|
4054
|
+
const o = t.querySelectorAll(`#${CSS.escape(n)}`), s = t.querySelector(
|
|
4055
|
+
W.map((c) => `[${c}~="${CSS.escape(n)}"]`).join(", ")
|
|
4056
|
+
), l = t.querySelector(`label[for="${CSS.escape(n)}"]`);
|
|
4057
|
+
let h;
|
|
4051
4058
|
if (s) {
|
|
4052
|
-
const c =
|
|
4059
|
+
const c = W.find(
|
|
4053
4060
|
(u) => {
|
|
4054
|
-
var
|
|
4055
|
-
return (
|
|
4061
|
+
var p;
|
|
4062
|
+
return (p = s.getAttribute(u)) == null ? void 0 : p.split(/\s+/).includes(n);
|
|
4056
4063
|
}
|
|
4057
4064
|
);
|
|
4058
|
-
c && (
|
|
4059
|
-
} else l && (
|
|
4065
|
+
c && (h = c);
|
|
4066
|
+
} else l && (h = "label[for]");
|
|
4060
4067
|
a.push({
|
|
4061
4068
|
ruleId: "duplicate-id-aria",
|
|
4062
4069
|
selector: m(o[1]),
|
|
4063
4070
|
html: d(o[1]),
|
|
4064
4071
|
impact: "critical",
|
|
4065
|
-
message: `Duplicate ID "${n}" referenced by ${
|
|
4066
|
-
context: `First element: ${d(o[0])}${
|
|
4067
|
-
Referenced by: ${
|
|
4072
|
+
message: `Duplicate ID "${n}" referenced by ${h ?? "an accessibility attribute"}.`,
|
|
4073
|
+
context: `First element: ${d(o[0])}${h ? `
|
|
4074
|
+
Referenced by: ${h}` : ""}`
|
|
4068
4075
|
});
|
|
4069
4076
|
}
|
|
4070
4077
|
return a;
|
|
@@ -4076,14 +4083,14 @@ Referenced by: ${p}` : ""}`
|
|
|
4076
4083
|
description: "Video elements must have captions via <track kind='captions'>.",
|
|
4077
4084
|
guidance: "Captions provide text alternatives for audio content in videos, benefiting deaf users and those who cannot hear audio. Add a <track> element with kind='captions' pointing to a WebVTT caption file. Captions should include both dialogue and important sound effects.",
|
|
4078
4085
|
prompt: "Explain how to add a captions track element to this video.",
|
|
4079
|
-
run(
|
|
4086
|
+
run(t) {
|
|
4080
4087
|
const a = [];
|
|
4081
|
-
for (const
|
|
4082
|
-
if (g(
|
|
4083
|
-
|
|
4088
|
+
for (const e of t.querySelectorAll("video")) {
|
|
4089
|
+
if (g(e) || e.hasAttribute("muted") || e.hasAttribute("autoplay")) continue;
|
|
4090
|
+
e.querySelector('track[kind="captions"], track[kind="subtitles"]') || a.push({
|
|
4084
4091
|
ruleId: "video-caption",
|
|
4085
|
-
selector: m(
|
|
4086
|
-
html: d(
|
|
4092
|
+
selector: m(e),
|
|
4093
|
+
html: d(e),
|
|
4087
4094
|
impact: "critical",
|
|
4088
4095
|
message: "Video element has no captions track."
|
|
4089
4096
|
});
|
|
@@ -4097,22 +4104,22 @@ Referenced by: ${p}` : ""}`
|
|
|
4097
4104
|
description: "Audio elements should have a text alternative or transcript.",
|
|
4098
4105
|
guidance: "Audio-only content like podcasts or recordings needs a text alternative for deaf users. Provide a transcript either on the same page or linked nearby. The transcript should include all spoken content and descriptions of relevant sounds.",
|
|
4099
4106
|
prompt: "Explain options for providing a text alternative: transcript link or aria-describedby.",
|
|
4100
|
-
run(
|
|
4107
|
+
run(t) {
|
|
4101
4108
|
const a = [];
|
|
4102
|
-
for (const
|
|
4103
|
-
if (g(
|
|
4104
|
-
const n =
|
|
4109
|
+
for (const e of t.querySelectorAll("audio")) {
|
|
4110
|
+
if (g(e) || e.querySelector('track[kind="captions"], track[kind="descriptions"]') || e.hasAttribute("aria-describedby")) continue;
|
|
4111
|
+
const n = e.parentElement;
|
|
4105
4112
|
n && n.querySelector('a[href*="transcript"], a[href*="text"]') || a.push({
|
|
4106
4113
|
ruleId: "audio-caption",
|
|
4107
|
-
selector: m(
|
|
4108
|
-
html: d(
|
|
4114
|
+
selector: m(e),
|
|
4115
|
+
html: d(e),
|
|
4109
4116
|
impact: "critical",
|
|
4110
4117
|
message: "Audio element has no transcript or text alternative. Add a transcript or track element."
|
|
4111
4118
|
});
|
|
4112
4119
|
}
|
|
4113
4120
|
return a;
|
|
4114
4121
|
}
|
|
4115
|
-
},
|
|
4122
|
+
}, yi = /* @__PURE__ */ new Set([
|
|
4116
4123
|
"SCRIPT",
|
|
4117
4124
|
"STYLE",
|
|
4118
4125
|
"NOSCRIPT",
|
|
@@ -4128,53 +4135,53 @@ Referenced by: ${p}` : ""}`
|
|
|
4128
4135
|
"BR",
|
|
4129
4136
|
"HR"
|
|
4130
4137
|
]);
|
|
4131
|
-
function ue([
|
|
4132
|
-
return "#" + [
|
|
4138
|
+
function ue([t, a, e]) {
|
|
4139
|
+
return "#" + [t, a, e].map((i) => i.toString(16).padStart(2, "0")).join("");
|
|
4133
4140
|
}
|
|
4134
|
-
function
|
|
4135
|
-
return
|
|
4141
|
+
function wi(t) {
|
|
4142
|
+
return t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement || t instanceof HTMLButtonElement ? t.disabled : !!(t.closest("fieldset[disabled]") || t.getAttribute("aria-disabled") === "true");
|
|
4136
4143
|
}
|
|
4137
|
-
function Ai(
|
|
4138
|
-
if (
|
|
4139
|
-
const
|
|
4144
|
+
function Ai(t, a) {
|
|
4145
|
+
if (t.tagName !== "LABEL") return !1;
|
|
4146
|
+
const e = t, i = e.htmlFor;
|
|
4140
4147
|
if (i) {
|
|
4141
4148
|
const o = a.getElementById(i);
|
|
4142
4149
|
if (o && (o.disabled || o.getAttribute("aria-disabled") === "true")) return !0;
|
|
4143
4150
|
}
|
|
4144
|
-
const n =
|
|
4151
|
+
const n = e.querySelector("input, select, textarea, button");
|
|
4145
4152
|
if (n && (n.disabled || n.getAttribute("aria-disabled") === "true")) return !0;
|
|
4146
|
-
const r =
|
|
4153
|
+
const r = e.id;
|
|
4147
4154
|
return !!(r && a.querySelector(`[aria-labelledby~="${r}"][aria-disabled="true"]`));
|
|
4148
4155
|
}
|
|
4149
|
-
function xi(
|
|
4150
|
-
const a =
|
|
4156
|
+
function xi(t) {
|
|
4157
|
+
const a = t.clip;
|
|
4151
4158
|
if (a && a.startsWith("rect(")) {
|
|
4152
4159
|
const i = a.match(/[\d.]+/g);
|
|
4153
4160
|
if (!i || i.every((n) => parseFloat(n) === 0)) return !0;
|
|
4154
4161
|
}
|
|
4155
|
-
const
|
|
4156
|
-
if (
|
|
4157
|
-
if (
|
|
4158
|
-
const i = parseFloat(
|
|
4162
|
+
const e = t.clipPath;
|
|
4163
|
+
if (e === "inset(50%)" || e === "inset(100%)") return !0;
|
|
4164
|
+
if (t.overflow === "hidden" && t.position === "absolute") {
|
|
4165
|
+
const i = parseFloat(t.width), n = parseFloat(t.height);
|
|
4159
4166
|
if (i <= 1 && n <= 1) return !0;
|
|
4160
4167
|
}
|
|
4161
4168
|
return !1;
|
|
4162
4169
|
}
|
|
4163
|
-
function Si(
|
|
4164
|
-
if (g(
|
|
4165
|
-
let a =
|
|
4170
|
+
function Si(t) {
|
|
4171
|
+
if (g(t)) return !0;
|
|
4172
|
+
let a = t;
|
|
4166
4173
|
for (; a; ) {
|
|
4167
|
-
const
|
|
4168
|
-
if (
|
|
4174
|
+
const e = w(a);
|
|
4175
|
+
if (e.display === "none" || e.visibility === "hidden" || xi(e)) return !0;
|
|
4169
4176
|
a = a.parentElement;
|
|
4170
4177
|
}
|
|
4171
4178
|
return !1;
|
|
4172
4179
|
}
|
|
4173
|
-
function ki(
|
|
4174
|
-
let a = 1,
|
|
4175
|
-
for (;
|
|
4176
|
-
const i =
|
|
4177
|
-
isNaN(n) || (a *= n),
|
|
4180
|
+
function ki(t) {
|
|
4181
|
+
let a = 1, e = t;
|
|
4182
|
+
for (; e; ) {
|
|
4183
|
+
const i = w(e), n = parseFloat(i.opacity);
|
|
4184
|
+
isNaN(n) || (a *= n), e = e.parentElement;
|
|
4178
4185
|
}
|
|
4179
4186
|
return a;
|
|
4180
4187
|
}
|
|
@@ -4189,35 +4196,35 @@ const Ii = {
|
|
|
4189
4196
|
saturate: 1,
|
|
4190
4197
|
opacity: 1
|
|
4191
4198
|
};
|
|
4192
|
-
function Ti(
|
|
4193
|
-
const a = parseFloat(
|
|
4194
|
-
return isNaN(a) ? NaN :
|
|
4199
|
+
function Ti(t) {
|
|
4200
|
+
const a = parseFloat(t);
|
|
4201
|
+
return isNaN(a) ? NaN : t.trim().endsWith("%") ? a / 100 : a;
|
|
4195
4202
|
}
|
|
4196
4203
|
const de = /([a-z-]+)\(([^)]*)\)/g;
|
|
4197
|
-
function me(
|
|
4198
|
-
let a,
|
|
4199
|
-
for (de.lastIndex = 0; a = de.exec(
|
|
4200
|
-
|
|
4204
|
+
function me(t) {
|
|
4205
|
+
let a, e = !1;
|
|
4206
|
+
for (de.lastIndex = 0; a = de.exec(t); ) {
|
|
4207
|
+
e = !0;
|
|
4201
4208
|
const i = Ii[a[1]];
|
|
4202
4209
|
if (i === void 0 || Ti(a[2]) !== i) return !1;
|
|
4203
4210
|
}
|
|
4204
|
-
return
|
|
4211
|
+
return e;
|
|
4205
4212
|
}
|
|
4206
|
-
function Ei(
|
|
4207
|
-
let a =
|
|
4213
|
+
function Ei(t) {
|
|
4214
|
+
let a = t;
|
|
4208
4215
|
for (; a; ) {
|
|
4209
|
-
const
|
|
4216
|
+
const e = w(a), i = e.filter;
|
|
4210
4217
|
if (i && i !== "none" && i !== "initial" && !me(i)) return !0;
|
|
4211
|
-
const n =
|
|
4218
|
+
const n = e.mixBlendMode;
|
|
4212
4219
|
if (n && n !== "normal" && n !== "initial") return !0;
|
|
4213
|
-
const r =
|
|
4220
|
+
const r = e.backdropFilter;
|
|
4214
4221
|
if (r && r !== "none" && r !== "initial" && !me(r)) return !0;
|
|
4215
4222
|
a = a.parentElement;
|
|
4216
4223
|
}
|
|
4217
4224
|
return !1;
|
|
4218
4225
|
}
|
|
4219
|
-
function Ci(
|
|
4220
|
-
return
|
|
4226
|
+
function Ci(t) {
|
|
4227
|
+
return t.closest("select") !== null;
|
|
4221
4228
|
}
|
|
4222
4229
|
const Li = {
|
|
4223
4230
|
id: "color-contrast",
|
|
@@ -4226,37 +4233,37 @@ const Li = {
|
|
|
4226
4233
|
description: "Text elements must have sufficient color contrast against the background.",
|
|
4227
4234
|
guidance: "WCAG SC 1.4.3 requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (>=24px or >=18.66px bold). Increase the contrast by darkening the text or lightening the background, or vice versa.",
|
|
4228
4235
|
prompt: "Suggest changing the text or background color to meet the minimum contrast ratio.",
|
|
4229
|
-
run(
|
|
4230
|
-
const a = [],
|
|
4231
|
-
if (!
|
|
4232
|
-
const i =
|
|
4236
|
+
run(t) {
|
|
4237
|
+
const a = [], e = t.body;
|
|
4238
|
+
if (!e) return [];
|
|
4239
|
+
const i = t.createTreeWalker(e, NodeFilter.SHOW_TEXT), n = /* @__PURE__ */ new Set();
|
|
4233
4240
|
let r;
|
|
4234
4241
|
for (; r = i.nextNode(); ) {
|
|
4235
4242
|
if (!r.textContent || !r.textContent.trim()) continue;
|
|
4236
4243
|
const o = r.parentElement;
|
|
4237
|
-
if (!o || n.has(o) || (n.add(o),
|
|
4244
|
+
if (!o || n.has(o) || (n.add(o), yi.has(o.tagName))) continue;
|
|
4238
4245
|
const s = o.tagName;
|
|
4239
|
-
if (s === "BODY" || s === "HTML" || Ci(o) ||
|
|
4240
|
-
const l =
|
|
4246
|
+
if (s === "BODY" || s === "HTML" || Ci(o) || wi(o) || Ai(o, t) || Si(o)) continue;
|
|
4247
|
+
const l = w(o);
|
|
4241
4248
|
if (parseFloat(l.opacity) === 0 || ki(o) < 0.1) continue;
|
|
4242
|
-
const
|
|
4243
|
-
if (
|
|
4249
|
+
const h = l.textShadow;
|
|
4250
|
+
if (h && h !== "none" && h !== "initial" || Ei(o)) continue;
|
|
4244
4251
|
const c = N(l.color);
|
|
4245
4252
|
if (!c) continue;
|
|
4246
4253
|
const u = l.color.match(/rgba\(.+?,\s*([\d.]+)\s*\)/) || l.color.match(/rgba?\(.+?\/\s*([\d.]+%?)\s*\)/);
|
|
4247
4254
|
if (u && (u[1].endsWith("%") ? parseFloat(u[1]) / 100 : parseFloat(u[1])) === 0 || je(o)) continue;
|
|
4248
|
-
const
|
|
4249
|
-
if (!
|
|
4250
|
-
const b = R(c[0], c[1], c[2]), f = R(
|
|
4251
|
-
if (
|
|
4252
|
-
const
|
|
4255
|
+
const p = Be(o);
|
|
4256
|
+
if (!p) continue;
|
|
4257
|
+
const b = R(c[0], c[1], c[2]), f = R(p[0], p[1], p[2]), y = ge(b, f), x = Ue(o) ? 3 : 4.5;
|
|
4258
|
+
if (y < x) {
|
|
4259
|
+
const S = Math.round(y * 100) / 100, M = ue(c), xe = ue(p);
|
|
4253
4260
|
a.push({
|
|
4254
4261
|
ruleId: "color-contrast",
|
|
4255
4262
|
selector: m(o),
|
|
4256
4263
|
html: d(o),
|
|
4257
4264
|
impact: "serious",
|
|
4258
|
-
message: `Insufficient color contrast ratio of ${
|
|
4259
|
-
context: `foreground: ${M} rgb(${c.join(", ")}), background: ${xe} rgb(${
|
|
4265
|
+
message: `Insufficient color contrast ratio of ${S}:1 (required ${x}:1).`,
|
|
4266
|
+
context: `foreground: ${M} rgb(${c.join(", ")}), background: ${xe} rgb(${p.join(", ")}), ratio: ${S}:1, required: ${x}:1`
|
|
4260
4267
|
});
|
|
4261
4268
|
}
|
|
4262
4269
|
}
|
|
@@ -4336,7 +4343,7 @@ const Li = {
|
|
|
4336
4343
|
Pa,
|
|
4337
4344
|
ja,
|
|
4338
4345
|
Ua,
|
|
4339
|
-
|
|
4346
|
+
wa,
|
|
4340
4347
|
Ga,
|
|
4341
4348
|
// Links
|
|
4342
4349
|
Xa,
|
|
@@ -4361,23 +4368,23 @@ const Li = {
|
|
|
4361
4368
|
// Color
|
|
4362
4369
|
Li
|
|
4363
4370
|
];
|
|
4364
|
-
let
|
|
4365
|
-
function $i(
|
|
4366
|
-
|
|
4371
|
+
let Y = [], ye = /* @__PURE__ */ new Set();
|
|
4372
|
+
function $i(t) {
|
|
4373
|
+
t.additionalRules && (Y = t.additionalRules), t.disabledRules && (ye = new Set(t.disabledRules));
|
|
4367
4374
|
}
|
|
4368
|
-
function
|
|
4369
|
-
return ve.filter((a) => !
|
|
4375
|
+
function we() {
|
|
4376
|
+
return ve.filter((a) => !ye.has(a.id)).concat(Y);
|
|
4370
4377
|
}
|
|
4371
|
-
function Mi(
|
|
4378
|
+
function Mi(t) {
|
|
4372
4379
|
Ae();
|
|
4373
|
-
const a =
|
|
4380
|
+
const a = we(), e = [];
|
|
4374
4381
|
let i = 0;
|
|
4375
4382
|
return {
|
|
4376
4383
|
processChunk(n) {
|
|
4377
4384
|
const r = performance.now();
|
|
4378
4385
|
for (; i < a.length; ) {
|
|
4379
4386
|
try {
|
|
4380
|
-
|
|
4387
|
+
e.push(...a[i].run(t));
|
|
4381
4388
|
} catch {
|
|
4382
4389
|
}
|
|
4383
4390
|
if (i++, performance.now() - r >= n) break;
|
|
@@ -4385,33 +4392,33 @@ function Mi(e) {
|
|
|
4385
4392
|
return i < a.length;
|
|
4386
4393
|
},
|
|
4387
4394
|
getViolations() {
|
|
4388
|
-
return
|
|
4395
|
+
return e;
|
|
4389
4396
|
}
|
|
4390
4397
|
};
|
|
4391
4398
|
}
|
|
4392
4399
|
function Ae() {
|
|
4393
4400
|
Ce(), Se(), ke(), Oe(), We(), Le();
|
|
4394
4401
|
}
|
|
4395
|
-
function Hi(
|
|
4402
|
+
function Hi(t) {
|
|
4396
4403
|
var i;
|
|
4397
4404
|
Ae();
|
|
4398
|
-
const a =
|
|
4405
|
+
const a = we(), e = [];
|
|
4399
4406
|
for (const n of a)
|
|
4400
4407
|
try {
|
|
4401
|
-
|
|
4408
|
+
e.push(...n.run(t));
|
|
4402
4409
|
} catch {
|
|
4403
4410
|
}
|
|
4404
4411
|
return {
|
|
4405
|
-
url: ((i =
|
|
4412
|
+
url: ((i = t.location) == null ? void 0 : i.href) ?? "",
|
|
4406
4413
|
timestamp: Date.now(),
|
|
4407
|
-
violations:
|
|
4414
|
+
violations: e,
|
|
4408
4415
|
ruleCount: a.length
|
|
4409
4416
|
};
|
|
4410
4417
|
}
|
|
4411
|
-
const qi = new Map(ve.map((
|
|
4412
|
-
function Di(
|
|
4413
|
-
const a = qi.get(
|
|
4414
|
-
return a ||
|
|
4418
|
+
const qi = new Map(ve.map((t) => [t.id, t]));
|
|
4419
|
+
function Di(t) {
|
|
4420
|
+
const a = qi.get(t);
|
|
4421
|
+
return a || Y.find((e) => e.id === t);
|
|
4415
4422
|
}
|
|
4416
4423
|
export {
|
|
4417
4424
|
Ae as clearAllCaches,
|
|
@@ -4419,12 +4426,12 @@ export {
|
|
|
4419
4426
|
Ce as clearAriaHiddenCache,
|
|
4420
4427
|
Oe as clearColorCaches,
|
|
4421
4428
|
Se as clearComputedRoleCache,
|
|
4422
|
-
|
|
4429
|
+
I as compileDeclarativeRule,
|
|
4423
4430
|
$i as configureRules,
|
|
4424
4431
|
Mi as createChunkedAudit,
|
|
4425
4432
|
v as getAccessibleName,
|
|
4426
4433
|
A as getAccessibleTextContent,
|
|
4427
|
-
|
|
4434
|
+
we as getActiveRules,
|
|
4428
4435
|
q as getComputedRole,
|
|
4429
4436
|
d as getHtmlSnippet,
|
|
4430
4437
|
he as getImplicitRole,
|