ultra11y 5.26.0 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.26.0",
13
+ "version": "5.28.0",
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.26.0",
4
+ "version": "5.28.0",
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.26.0",
4
+ "version": "5.28.0",
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.26.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.28.0";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/README.md CHANGED
@@ -371,7 +371,7 @@ a test). See [`CONTRIBUTING.md`](CONTRIBUTING.md) and `skills/ultra11y/reference
371
371
  fills an input or resizes the viewport — otherwise the snapshot would record our own
372
372
  instrumentation instead of the site.
373
373
  - **Page by page** — RGAA is a per-page norm; the engine's verdict is scope-wide. `pages`
374
- bridges the two: one row per criterion, one column per page, embedded in `report` and
374
+ bridges the two: one row per criterion, one column per page URL, embedded in `report` and
375
375
  rebuilt from a committed `audit.json` alone. The per-page status is not recomputed — a
376
376
  per-page *view* of the audit runs through the very same projection the report uses, so grid
377
377
  and report agree by construction. Two rules hold the line: a finding is attributed to a page
@@ -287,14 +287,17 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
287
287
  if (!count) return [];
288
288
  const hits = [];
289
289
  const seen = /* @__PURE__ */ new Set();
290
- const limit = Math.min(count + 2, limits.maxFocusables + 10);
290
+ const limit = tabPressBudget(count, limits);
291
+ let prevKey = null;
291
292
  for (let i = 0; i < limit; i++) {
292
293
  if (deadline?.out()) break;
293
294
  await page.keyboard.press("Tab");
294
295
  const r = await page.evaluate(FOCUS_CHECK_PROBE);
295
296
  if (!r) continue;
297
+ if (r.key === prevKey) continue;
296
298
  if (seen.has(r.key)) break;
297
299
  seen.add(r.key);
300
+ prevKey = r.key;
298
301
  if (!r.changed) {
299
302
  hits.push({
300
303
  selector: r.selector,
@@ -306,11 +309,23 @@ async function probeFocusVisible(page, scope = "", limits = PROBE_DEFAULTS, dead
306
309
  }
307
310
  return hits;
308
311
  }
312
+ var NATIVE_SEGMENT_STOPS = {
313
+ date: 5,
314
+ time: 5,
315
+ "datetime-local": 8,
316
+ month: 4,
317
+ week: 4
318
+ };
319
+ function tabPressBudget(count, limits) {
320
+ return Math.min(count * 2 + 20, limits.maxFocusables * 2 + 20);
321
+ }
309
322
  var FOCUS_WHERE_PROBE = `(() => { ${PRELUDE}
310
323
  const e = document.activeElement;
311
324
  if (!e || e === document.body || e === document.documentElement) return null;
312
325
  const key = e.getAttribute && e.getAttribute('data-u11y-f');
313
- return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e) };
326
+ const stops = ${JSON.stringify(NATIVE_SEGMENT_STOPS)};
327
+ const type = e.tagName === 'INPUT' ? (e.getAttribute('type') || 'text').toLowerCase() : '';
328
+ return { key: key || __sel(e), tagged: !!key, selector: __sel(e), html: __html(e), segments: stops[type] || 1 };
314
329
  })()`;
315
330
  async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
316
331
  const count = await page.evaluate(focusSetupExpr("", limits.maxFocusables));
@@ -318,7 +333,7 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
318
333
  const hits = [];
319
334
  const seen = /* @__PURE__ */ new Set();
320
335
  const confirmPresses = 2;
321
- const limit = Math.min(count + 2, limits.maxFocusables + 10);
336
+ const limit = tabPressBudget(count, limits);
322
337
  let prev = null;
323
338
  for (let i = 0; i < limit; i++) {
324
339
  if (deadline?.out()) break;
@@ -326,8 +341,9 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
326
341
  const now = await page.evaluate(FOCUS_WHERE_PROBE);
327
342
  if (!now) break;
328
343
  if (prev?.tagged && now.tagged && now.key === prev.key) {
344
+ const budget = Math.max(confirmPresses, (now.segments ?? 1) - 1);
329
345
  let stuck = true;
330
- for (let k = 0; k < confirmPresses && stuck; k++) {
346
+ for (let k = 0; k < budget && stuck; k++) {
331
347
  if (deadline?.out()) break;
332
348
  await page.keyboard.press("Tab");
333
349
  const again = await page.evaluate(FOCUS_WHERE_PROBE);
@@ -337,13 +353,15 @@ async function probeKeyboardTrap(page, limits = PROBE_DEFAULTS, deadline) {
337
353
  hits.push({
338
354
  selector: now.selector,
339
355
  html: now.html,
340
- detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 + confirmPresses} appuis sur Tab, alors que la page compte ${count} \xE9l\xE9ments focalisables \u2014 pi\xE8ge au clavier (2.1.2).`
356
+ detail: `Le focus reste sur cet \xE9l\xE9ment apr\xE8s ${1 + budget} appuis sur Tab, alors que la page compte ${count} \xE9l\xE9ments focalisables \u2014 pi\xE8ge au clavier (2.1.2).`
341
357
  });
342
358
  break;
343
359
  }
344
360
  }
345
- if (seen.has(now.key)) break;
346
- seen.add(now.key);
361
+ if (now.key !== prev?.key) {
362
+ if (seen.has(now.key)) break;
363
+ seen.add(now.key);
364
+ }
347
365
  prev = now;
348
366
  }
349
367
  return hits;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.26.0",
3
+ "version": "5.28.0",
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",
@@ -251,9 +251,16 @@ const RULE_TO_CRITERIA = {
251
251
  "error-not-associated": ["11.10"], "aria-invalid-no-description": ["11.10"],
252
252
  "axe:label": ["11.1"], "axe:form-field-multiple-labels": ["11.1"], "axe:select-name": ["11.1"], "axe:label-title-only": ["11.1"],
253
253
  "axe:autocomplete-valid": ["11.13"], "axe:fieldset": ["11.6"], "axe:input-button-name": ["11.9"], "axe:button-name": ["11.9"],
254
- // Theme 12 — navigation (12.7 skip link, 12.8 tab order)
254
+ // Theme 12 — navigation (12.7 skip link, 12.8 tab order, 12.9 keyboard trap)
255
255
  "skip-link-target-missing": ["12.7"], "axe:skip-link": ["12.7"], "axe:bypass": ["12.7"],
256
256
  "positive-tabindex": ["12.8"], "axe:tabindex": ["12.8"],
257
+ // 12.9 « la navigation ne doit pas contenir de piège au clavier ». The probe that answers it
258
+ // has existed since `probeKeyboardTrap` landed, and it was mapped to WCAG 2.1.2 alone — so a
259
+ // trap the browser had just walked into raised a blocking WCAG non-conformity and left RGAA
260
+ // 12.9 « à évaluer », to be bought from a paid adjudicator on every run. The evidence was
261
+ // measured and then never delivered to the criterion it answers. No axe rule joins it: axe
262
+ // has none for keyboard traps, which is why the probe was written in the first place.
263
+ "dyn-keyboard-trap": ["12.9"],
257
264
  // Theme 13 — consultation (13.1 time limits, 13.8 moving/blinking)
258
265
  "meta-refresh-redirect": ["13.1"], "blink-marquee": ["13.8"], "autoplay-media": ["4.10", "13.8"],
259
266
  "axe:no-autoplay-audio": ["4.10"], "axe:blink": ["13.8"], "axe:marquee": ["13.8"],