ultra11y 5.22.0 → 5.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/dist/playwright.mjs +4 -6
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +21 -7
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +21 -7
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +21 -7
- package/src/adjudicate-subjects.ts +21 -0
- package/src/adjudicate.ts +2 -2
- package/src/probes.ts +13 -9
- package/src/types.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "5.22.
|
|
13
|
+
"version": "5.22.1",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.22.
|
|
4
|
+
"version": "5.22.1",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.22.
|
|
4
|
+
"version": "5.22.1",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.22.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.22.1";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/dist/playwright.mjs
CHANGED
|
@@ -310,7 +310,7 @@ var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
|
310
310
|
const e = document.activeElement;
|
|
311
311
|
if (!e || e === document.body || e === document.documentElement) return null;
|
|
312
312
|
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
313
|
-
return { key: key || __sel(e), selector: __sel(e), html: __html(e) };
|
|
313
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
|
|
314
314
|
})()`;
|
|
315
315
|
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
316
316
|
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
@@ -325,13 +325,13 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
325
325
|
await page.keyboard.press("Tab");
|
|
326
326
|
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
327
327
|
if (!now) break;
|
|
328
|
-
if (prev && now.key === prev.key) {
|
|
328
|
+
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
329
329
|
let stuck = true;
|
|
330
330
|
for (let k = 0; k < confirmPresses && stuck; k++) {
|
|
331
331
|
if (deadline?.out()) break;
|
|
332
332
|
await page.keyboard.press("Tab");
|
|
333
333
|
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
334
|
-
stuck = again !== null && again.key === now.key;
|
|
334
|
+
stuck = again !== null && again.tagged === true && again.key === now.key;
|
|
335
335
|
}
|
|
336
336
|
if (stuck) {
|
|
337
337
|
hits.push({
|
|
@@ -345,7 +345,6 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
345
345
|
if (seen.has(now.key)) break;
|
|
346
346
|
seen.add(now.key);
|
|
347
347
|
prev = now;
|
|
348
|
-
if (hits.length >= 4) break;
|
|
349
348
|
}
|
|
350
349
|
return hits;
|
|
351
350
|
}
|
|
@@ -432,8 +431,7 @@ async function runLiveProbes(page, opts = {}) {
|
|
|
432
431
|
}
|
|
433
432
|
};
|
|
434
433
|
if (!canResize) skip("1.4.10", "the page object cannot resize its viewport");
|
|
435
|
-
if (!canType)
|
|
436
|
-
if (!canType) skip("2.1.2", "the page object exposes no keyboard");
|
|
434
|
+
if (!canType) for (const sc of ["2.4.7", "2.1.2"]) skip(sc, "the page object exposes no keyboard");
|
|
437
435
|
if (!canHover) skip("1.4.13", "the page object cannot hover");
|
|
438
436
|
if (!canStyle) skip("1.4.12", "the page object cannot inject a stylesheet");
|
|
439
437
|
if (want("1.4.4")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.22.
|
|
3
|
+
"version": "5.22.1",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.22.
|
|
9
|
+
var VERSION = "5.22.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -39953,6 +39953,21 @@ var SUBJECTS = {
|
|
|
39953
39953
|
(l) => hAt(d, l.line, "css", `CSS rule keyed on a pointer/keyboard state \u2014 does the hover half have a focus twin? ${l.text}`, `cssstate|${l.text}`)
|
|
39954
39954
|
)
|
|
39955
39955
|
);
|
|
39956
|
+
if (out2.length) {
|
|
39957
|
+
for (const link of elementsByTag(d, "link")) {
|
|
39958
|
+
const rel2 = (attr(link, "rel") ?? "").toLowerCase();
|
|
39959
|
+
if (!rel2.split(/\s+/).includes("stylesheet")) continue;
|
|
39960
|
+
const href = attr(link, "href") ?? "";
|
|
39961
|
+
out2.push(
|
|
39962
|
+
h(
|
|
39963
|
+
d,
|
|
39964
|
+
link,
|
|
39965
|
+
`stylesheet "${href}" \u2014 this page reveals additional content and the rule that does it is NOT in this file. Open the stylesheet and check the \`:hover\` selectors for a \`:focus\`/\`:focus-within\` twin.`,
|
|
39966
|
+
`stylesheet|${href}`
|
|
39967
|
+
)
|
|
39968
|
+
);
|
|
39969
|
+
}
|
|
39970
|
+
}
|
|
39956
39971
|
return out2;
|
|
39957
39972
|
}),
|
|
39958
39973
|
// Content pinned over the page — what can obscure a focused element.
|
|
@@ -58021,8 +58036,8 @@ function adjudicationContract() {
|
|
|
58021
58036
|
manualReasons: [...MANUAL_REASON_VALUES],
|
|
58022
58037
|
requires: {
|
|
58023
58038
|
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence, and about the same kind of element the harvest recorded there \u2014 copy the evidence's own `snippet` rather than retyping the element); a criterion with no harvested evidence cannot be C at all",
|
|
58024
|
-
NA: "a non-empty justification",
|
|
58025
|
-
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
58039
|
+
NA: "a non-empty justification, and citations[] whenever evidence was presented \u2014 a criterion whose subject exists NOWHERE in the audited scope is NA, never NC",
|
|
58040
|
+
NC: "at least one groundable finding, each naming the `file` it was observed in (an NC with no location is refused as flatly as an uncited C \u2014 and a non-conformity that rests on an ABSENCE is still observed on an element of a page, so cite that element) and each citing a normativeRef that resolves against the active standard",
|
|
58026
58041
|
manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
|
|
58027
58042
|
}
|
|
58028
58043
|
};
|
|
@@ -60073,7 +60088,7 @@ var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
|
60073
60088
|
const e = document.activeElement;
|
|
60074
60089
|
if (!e || e === document.body || e === document.documentElement) return null;
|
|
60075
60090
|
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
60076
|
-
return { key: key || __sel(e), selector: __sel(e), html: __html(e) };
|
|
60091
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
|
|
60077
60092
|
})()`;
|
|
60078
60093
|
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
60079
60094
|
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
@@ -60088,13 +60103,13 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
60088
60103
|
await page.keyboard.press("Tab");
|
|
60089
60104
|
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60090
60105
|
if (!now) break;
|
|
60091
|
-
if (prev && now.key === prev.key) {
|
|
60106
|
+
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
60092
60107
|
let stuck = true;
|
|
60093
60108
|
for (let k = 0; k < confirmPresses && stuck; k++) {
|
|
60094
60109
|
if (deadline?.out()) break;
|
|
60095
60110
|
await page.keyboard.press("Tab");
|
|
60096
60111
|
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60097
|
-
stuck = again !== null && again.key === now.key;
|
|
60112
|
+
stuck = again !== null && again.tagged === true && again.key === now.key;
|
|
60098
60113
|
}
|
|
60099
60114
|
if (stuck) {
|
|
60100
60115
|
hits.push({
|
|
@@ -60108,7 +60123,6 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
60108
60123
|
if (seen.has(now.key)) break;
|
|
60109
60124
|
seen.add(now.key);
|
|
60110
60125
|
prev = now;
|
|
60111
|
-
if (hits.length >= 4) break;
|
|
60112
60126
|
}
|
|
60113
60127
|
return hits;
|
|
60114
60128
|
}
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 5.22.
|
|
8
|
+
version: 5.22.1
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.22.
|
|
9
|
+
var VERSION = "5.22.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -39953,6 +39953,21 @@ var SUBJECTS = {
|
|
|
39953
39953
|
(l) => hAt(d, l.line, "css", `CSS rule keyed on a pointer/keyboard state \u2014 does the hover half have a focus twin? ${l.text}`, `cssstate|${l.text}`)
|
|
39954
39954
|
)
|
|
39955
39955
|
);
|
|
39956
|
+
if (out2.length) {
|
|
39957
|
+
for (const link of elementsByTag(d, "link")) {
|
|
39958
|
+
const rel2 = (attr(link, "rel") ?? "").toLowerCase();
|
|
39959
|
+
if (!rel2.split(/\s+/).includes("stylesheet")) continue;
|
|
39960
|
+
const href = attr(link, "href") ?? "";
|
|
39961
|
+
out2.push(
|
|
39962
|
+
h(
|
|
39963
|
+
d,
|
|
39964
|
+
link,
|
|
39965
|
+
`stylesheet "${href}" \u2014 this page reveals additional content and the rule that does it is NOT in this file. Open the stylesheet and check the \`:hover\` selectors for a \`:focus\`/\`:focus-within\` twin.`,
|
|
39966
|
+
`stylesheet|${href}`
|
|
39967
|
+
)
|
|
39968
|
+
);
|
|
39969
|
+
}
|
|
39970
|
+
}
|
|
39956
39971
|
return out2;
|
|
39957
39972
|
}),
|
|
39958
39973
|
// Content pinned over the page — what can obscure a focused element.
|
|
@@ -58021,8 +58036,8 @@ function adjudicationContract() {
|
|
|
58021
58036
|
manualReasons: [...MANUAL_REASON_VALUES],
|
|
58022
58037
|
requires: {
|
|
58023
58038
|
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence, and about the same kind of element the harvest recorded there \u2014 copy the evidence's own `snippet` rather than retyping the element); a criterion with no harvested evidence cannot be C at all",
|
|
58024
|
-
NA: "a non-empty justification",
|
|
58025
|
-
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
58039
|
+
NA: "a non-empty justification, and citations[] whenever evidence was presented \u2014 a criterion whose subject exists NOWHERE in the audited scope is NA, never NC",
|
|
58040
|
+
NC: "at least one groundable finding, each naming the `file` it was observed in (an NC with no location is refused as flatly as an uncited C \u2014 and a non-conformity that rests on an ABSENCE is still observed on an element of a page, so cite that element) and each citing a normativeRef that resolves against the active standard",
|
|
58026
58041
|
manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
|
|
58027
58042
|
}
|
|
58028
58043
|
};
|
|
@@ -60073,7 +60088,7 @@ var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
|
60073
60088
|
const e = document.activeElement;
|
|
60074
60089
|
if (!e || e === document.body || e === document.documentElement) return null;
|
|
60075
60090
|
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
60076
|
-
return { key: key || __sel(e), selector: __sel(e), html: __html(e) };
|
|
60091
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
|
|
60077
60092
|
})()`;
|
|
60078
60093
|
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
60079
60094
|
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
@@ -60088,13 +60103,13 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
60088
60103
|
await page.keyboard.press("Tab");
|
|
60089
60104
|
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60090
60105
|
if (!now) break;
|
|
60091
|
-
if (prev && now.key === prev.key) {
|
|
60106
|
+
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
60092
60107
|
let stuck = true;
|
|
60093
60108
|
for (let k = 0; k < confirmPresses && stuck; k++) {
|
|
60094
60109
|
if (deadline?.out()) break;
|
|
60095
60110
|
await page.keyboard.press("Tab");
|
|
60096
60111
|
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60097
|
-
stuck = again !== null && again.key === now.key;
|
|
60112
|
+
stuck = again !== null && again.tagged === true && again.key === now.key;
|
|
60098
60113
|
}
|
|
60099
60114
|
if (stuck) {
|
|
60100
60115
|
hits.push({
|
|
@@ -60108,7 +60123,6 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
60108
60123
|
if (seen.has(now.key)) break;
|
|
60109
60124
|
seen.add(now.key);
|
|
60110
60125
|
prev = now;
|
|
60111
|
-
if (hits.length >= 4) break;
|
|
60112
60126
|
}
|
|
60113
60127
|
return hits;
|
|
60114
60128
|
}
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
4
|
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 5.22.
|
|
7
|
+
version: 5.22.1
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.22.
|
|
9
|
+
var VERSION = "5.22.1";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -39953,6 +39953,21 @@ var SUBJECTS = {
|
|
|
39953
39953
|
(l) => hAt(d, l.line, "css", `CSS rule keyed on a pointer/keyboard state \u2014 does the hover half have a focus twin? ${l.text}`, `cssstate|${l.text}`)
|
|
39954
39954
|
)
|
|
39955
39955
|
);
|
|
39956
|
+
if (out2.length) {
|
|
39957
|
+
for (const link of elementsByTag(d, "link")) {
|
|
39958
|
+
const rel2 = (attr(link, "rel") ?? "").toLowerCase();
|
|
39959
|
+
if (!rel2.split(/\s+/).includes("stylesheet")) continue;
|
|
39960
|
+
const href = attr(link, "href") ?? "";
|
|
39961
|
+
out2.push(
|
|
39962
|
+
h(
|
|
39963
|
+
d,
|
|
39964
|
+
link,
|
|
39965
|
+
`stylesheet "${href}" \u2014 this page reveals additional content and the rule that does it is NOT in this file. Open the stylesheet and check the \`:hover\` selectors for a \`:focus\`/\`:focus-within\` twin.`,
|
|
39966
|
+
`stylesheet|${href}`
|
|
39967
|
+
)
|
|
39968
|
+
);
|
|
39969
|
+
}
|
|
39970
|
+
}
|
|
39956
39971
|
return out2;
|
|
39957
39972
|
}),
|
|
39958
39973
|
// Content pinned over the page — what can obscure a focused element.
|
|
@@ -58021,8 +58036,8 @@ function adjudicationContract() {
|
|
|
58021
58036
|
manualReasons: [...MANUAL_REASON_VALUES],
|
|
58022
58037
|
requires: {
|
|
58023
58038
|
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence, and about the same kind of element the harvest recorded there \u2014 copy the evidence's own `snippet` rather than retyping the element); a criterion with no harvested evidence cannot be C at all",
|
|
58024
|
-
NA: "a non-empty justification",
|
|
58025
|
-
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
58039
|
+
NA: "a non-empty justification, and citations[] whenever evidence was presented \u2014 a criterion whose subject exists NOWHERE in the audited scope is NA, never NC",
|
|
58040
|
+
NC: "at least one groundable finding, each naming the `file` it was observed in (an NC with no location is refused as flatly as an uncited C \u2014 and a non-conformity that rests on an ABSENCE is still observed on an element of a page, so cite that element) and each citing a normativeRef that resolves against the active standard",
|
|
58026
58041
|
manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
|
|
58027
58042
|
}
|
|
58028
58043
|
};
|
|
@@ -60073,7 +60088,7 @@ var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
|
60073
60088
|
const e = document.activeElement;
|
|
60074
60089
|
if (!e || e === document.body || e === document.documentElement) return null;
|
|
60075
60090
|
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
60076
|
-
return { key: key || __sel(e), selector: __sel(e), html: __html(e) };
|
|
60091
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
|
|
60077
60092
|
})()`;
|
|
60078
60093
|
async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
60079
60094
|
const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
|
|
@@ -60088,13 +60103,13 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
60088
60103
|
await page.keyboard.press("Tab");
|
|
60089
60104
|
const now = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60090
60105
|
if (!now) break;
|
|
60091
|
-
if (prev && now.key === prev.key) {
|
|
60106
|
+
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
60092
60107
|
let stuck = true;
|
|
60093
60108
|
for (let k = 0; k < confirmPresses && stuck; k++) {
|
|
60094
60109
|
if (deadline?.out()) break;
|
|
60095
60110
|
await page.keyboard.press("Tab");
|
|
60096
60111
|
const again = await page.evaluate(FOCUS_WHERE_PROBE);
|
|
60097
|
-
stuck = again !== null && again.key === now.key;
|
|
60112
|
+
stuck = again !== null && again.tagged === true && again.key === now.key;
|
|
60098
60113
|
}
|
|
60099
60114
|
if (stuck) {
|
|
60100
60115
|
hits.push({
|
|
@@ -60108,7 +60123,6 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
|
|
|
60108
60123
|
if (seen.has(now.key)) break;
|
|
60109
60124
|
seen.add(now.key);
|
|
60110
60125
|
prev = now;
|
|
60111
|
-
if (hits.length >= 4) break;
|
|
60112
60126
|
}
|
|
60113
60127
|
return hits;
|
|
60114
60128
|
}
|
|
@@ -783,6 +783,27 @@ export const SUBJECTS: Record<string, Subject> = {
|
|
|
783
783
|
hAt(d, l.line, "css", `CSS rule keyed on a pointer/keyboard state — does the hover half have a focus twin? ${l.text}`, `cssstate|${l.text}`),
|
|
784
784
|
),
|
|
785
785
|
);
|
|
786
|
+
// …AND WHERE TO GO WHEN THE RULE IS NOT IN THIS FILE. An external stylesheet is not a
|
|
787
|
+
// document this engine parses (`.css` is absent from glob.ts DEFAULT_EXT — engine scope is
|
|
788
|
+
// markup), so a site whose tooltip lives in `styles.css` hands the adjudicator the trigger
|
|
789
|
+
// and the panel and NOTHING about how either becomes visible. It has Read and Grep; what
|
|
790
|
+
// it lacked was the address. Only emitted when this document really carries additional
|
|
791
|
+
// content, so a page with a stylesheet and no tooltip stays silent.
|
|
792
|
+
if (out.length) {
|
|
793
|
+
for (const link of elementsByTag(d, "link")) {
|
|
794
|
+
const rel = (attr(link, "rel") ?? "").toLowerCase();
|
|
795
|
+
if (!rel.split(/\s+/).includes("stylesheet")) continue;
|
|
796
|
+
const href = attr(link, "href") ?? "";
|
|
797
|
+
out.push(
|
|
798
|
+
h(
|
|
799
|
+
d,
|
|
800
|
+
link,
|
|
801
|
+
`stylesheet "${href}" — this page reveals additional content and the rule that does it is NOT in this file. Open the stylesheet and check the \`:hover\` selectors for a \`:focus\`/\`:focus-within\` twin.`,
|
|
802
|
+
`stylesheet|${href}`,
|
|
803
|
+
),
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
786
807
|
return out;
|
|
787
808
|
}),
|
|
788
809
|
|
package/src/adjudicate.ts
CHANGED
|
@@ -185,8 +185,8 @@ export function adjudicationContract(): AdjudicationContract {
|
|
|
185
185
|
manualReasons: [...MANUAL_REASON_VALUES],
|
|
186
186
|
requires: {
|
|
187
187
|
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence, and about the same kind of element the harvest recorded there — copy the evidence's own `snippet` rather than retyping the element); a criterion with no harvested evidence cannot be C at all",
|
|
188
|
-
NA: "a non-empty justification",
|
|
189
|
-
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
188
|
+
NA: "a non-empty justification, and citations[] whenever evidence was presented — a criterion whose subject exists NOWHERE in the audited scope is NA, never NC",
|
|
189
|
+
NC: "at least one groundable finding, each naming the `file` it was observed in (an NC with no location is refused as flatly as an uncited C — and a non-conformity that rests on an ABSENCE is still observed on an element of a page, so cite that element) and each citing a normativeRef that resolves against the active standard",
|
|
190
190
|
manual: `a reason ∈ {${MANUAL_REASON_VALUES.join(", ")}}`,
|
|
191
191
|
},
|
|
192
192
|
};
|
package/src/probes.ts
CHANGED
|
@@ -279,11 +279,16 @@ export async function probeFocusVisible(page: Any, scope = "", limits: ProbeLimi
|
|
|
279
279
|
// `null` means focus has left the ring — body, documentElement, or nothing at all, which is
|
|
280
280
|
// what Playwright reports once Tab hands focus back to the browser chrome. That is the NORMAL
|
|
281
281
|
// end of a tab ring, never a trap, and conflating the two would report every well-behaved page.
|
|
282
|
+
//
|
|
283
|
+
// `tagged` decides whether the caller may COMPARE two observations. An element the setup pass
|
|
284
|
+
// never tagged is identified by its selector alone, and a selector is not an identity: two links
|
|
285
|
+
// in the same list share one, so a walk that compared them would report a trap on a page whose
|
|
286
|
+
// focus was moving perfectly well. Untagged means « cannot accuse ».
|
|
282
287
|
export const FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
|
|
283
288
|
const e = document.activeElement;
|
|
284
289
|
if (!e || e === document.body || e === document.documentElement) return null;
|
|
285
290
|
const key = e.getAttribute && e.getAttribute('data-u11y-f');
|
|
286
|
-
return { key: key || __sel(e), selector: __sel(e), html: __html(e) };
|
|
291
|
+
return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
|
|
287
292
|
})()`;
|
|
288
293
|
|
|
289
294
|
/** RGAA 12.9 / WCAG 2.1.2 — can the keyboard always LEAVE?
|
|
@@ -308,23 +313,24 @@ export async function probeKeyboardTrap(page: Any, limits: ProbeLimits = PROBE_D
|
|
|
308
313
|
const seen = new Set<string>();
|
|
309
314
|
const confirmPresses = 2;
|
|
310
315
|
const limit = Math.min(count + 2, limits.maxFocusables + 10);
|
|
311
|
-
|
|
316
|
+
type Where = { key: string; tagged?: boolean; selector: string; html: string };
|
|
317
|
+
let prev: Where | null = null;
|
|
312
318
|
for (let i = 0; i < limit; i++) {
|
|
313
319
|
if (deadline?.out()) break;
|
|
314
320
|
await page.keyboard.press("Tab");
|
|
315
|
-
const now = (await page.evaluate(FOCUS_WHERE_PROBE)) as
|
|
321
|
+
const now = (await page.evaluate(FOCUS_WHERE_PROBE)) as Where | null;
|
|
316
322
|
// Focus left the page. The ring ended the way it should; nothing to report and nothing
|
|
317
323
|
// left to walk.
|
|
318
324
|
if (!now) break;
|
|
319
|
-
if (prev && now.key === prev.key) {
|
|
325
|
+
if (prev?.tagged && now.tagged && now.key === prev.key) {
|
|
320
326
|
// Stuck for one press. Confirm before accusing: press again, and only call it a trap if
|
|
321
327
|
// focus is STILL on the same element every time.
|
|
322
328
|
let stuck = true;
|
|
323
329
|
for (let k = 0; k < confirmPresses && stuck; k++) {
|
|
324
330
|
if (deadline?.out()) break;
|
|
325
331
|
await page.keyboard.press("Tab");
|
|
326
|
-
const again = (await page.evaluate(FOCUS_WHERE_PROBE)) as
|
|
327
|
-
stuck = again !== null && again.key === now.key;
|
|
332
|
+
const again = (await page.evaluate(FOCUS_WHERE_PROBE)) as Where | null;
|
|
333
|
+
stuck = again !== null && again.tagged === true && again.key === now.key;
|
|
328
334
|
}
|
|
329
335
|
if (stuck) {
|
|
330
336
|
hits.push({
|
|
@@ -340,7 +346,6 @@ export async function probeKeyboardTrap(page: Any, limits: ProbeLimits = PROBE_D
|
|
|
340
346
|
if (seen.has(now.key)) break;
|
|
341
347
|
seen.add(now.key);
|
|
342
348
|
prev = now;
|
|
343
|
-
if (hits.length >= 4) break;
|
|
344
349
|
}
|
|
345
350
|
return hits;
|
|
346
351
|
}
|
|
@@ -460,8 +465,7 @@ export async function runLiveProbes(page: Any, opts: { only?: string[]; limits?:
|
|
|
460
465
|
}
|
|
461
466
|
};
|
|
462
467
|
if (!canResize) skip("1.4.10", "the page object cannot resize its viewport");
|
|
463
|
-
if (!canType)
|
|
464
|
-
if (!canType) skip("2.1.2", "the page object exposes no keyboard");
|
|
468
|
+
if (!canType) for (const sc of ["2.4.7", "2.1.2"]) skip(sc, "the page object exposes no keyboard");
|
|
465
469
|
if (!canHover) skip("1.4.13", "the page object cannot hover");
|
|
466
470
|
if (!canStyle) skip("1.4.12", "the page object cannot inject a stylesheet");
|
|
467
471
|
// ORDER IS DELIBERATE: cheap and deterministic first, interactive last.
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "5.22.
|
|
3
|
+
export const VERSION = "5.22.1";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|