a11y-hud 0.4.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/bookmarklet.html +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +63 -23
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +82 -75
- package/dist/index.umd.js.map +1 -1
- package/package.json +11 -10
- package/dist/index.cjs +0 -1273
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -146
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@ A framework-agnostic developer overlay that runs [axe-core](https://github.com/d
|
|
|
5
5
|
- **No DevTools required** — the HUD runs in the page itself, not as an extension.
|
|
6
6
|
- **Framework-agnostic** — works with any stack. Framework adapters available for React, Vue, Angular, Svelte, and Solid.
|
|
7
7
|
- **Interactive** — violation list with severity filters, click-to-highlight, keyboard navigation, and clipboard export.
|
|
8
|
-
- **
|
|
8
|
+
- **Eight built-in themes** — `auto` (follows `prefers-color-scheme`, promotes to `high-contrast` under `prefers-contrast: more`) plus `default` (dark), `light`, `high-contrast`, `github-dark`, `github-light`, `tokyo-night`, `solarized-dark`.
|
|
9
|
+
- **ESM + UMD** — import it from a bundler, or load `dist/index.umd.js` from a CDN. Safe to import in SSR code; the HUD mounts only in the browser.
|
|
9
10
|
|
|
10
11
|
## Install
|
|
11
12
|
|
|
@@ -37,10 +38,11 @@ hud.unmount();
|
|
|
37
38
|
|
|
38
39
|
| Option | Type | Default | Description |
|
|
39
40
|
|--------|------|---------|-------------|
|
|
40
|
-
| `theme` | `"auto"
|
|
41
|
+
| `theme` | `Theme` | `"auto"` | Panel theme — `"auto"`, `"default"`, `"light"`, `"high-contrast"`, `"github-dark"`, `"github-light"`, `"tokyo-night"`, `"solarized-dark"` |
|
|
41
42
|
| `scope` | `string \| Element` | `document.body` | Restrict scan to a subtree |
|
|
42
43
|
| `autoScan` | `boolean` | `true` | Auto-rescan on DOM mutations |
|
|
43
44
|
| `debounce` | `number` | `500` | Debounce delay in ms |
|
|
45
|
+
| `runOnly` | `string[]` | `[]` | Restrict axe to specific rule-set tags (e.g. `["wcag2a", "wcag2aa"]`) |
|
|
44
46
|
|
|
45
47
|
### Headless API
|
|
46
48
|
|
|
@@ -51,6 +53,8 @@ const results = await runScan(document.querySelector("#my-app"));
|
|
|
51
53
|
console.log(results.violations);
|
|
52
54
|
```
|
|
53
55
|
|
|
56
|
+
`runScan()` needs a real browser DOM (run it in the page or inside a Playwright `page.evaluate()`); it is not supported under Node/jsdom.
|
|
57
|
+
|
|
54
58
|
## Framework adapters
|
|
55
59
|
|
|
56
60
|
- React — [`@a11y-hud/react`](https://www.npmjs.com/package/@a11y-hud/react)
|
package/dist/bookmarklet.html
CHANGED
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
|
|
122
122
|
<div class="version-row">
|
|
123
123
|
<span>This installer uses <strong>latest</strong>. To pin a specific release:</span>
|
|
124
|
-
<a href="javascript:(function(){if(window.A11yHud){window.A11yHud.mount();return;}var s=document.createElement('script');s.src='https://cdn.jsdelivr.net/npm/a11y-hud@0.
|
|
124
|
+
<a href="javascript:(function(){if(window.A11yHud){window.A11yHud.mount();return;}var s=document.createElement('script');s.src='https://cdn.jsdelivr.net/npm/a11y-hud@1.0.0/dist/index.umd.js';s.onload=function(){window.A11yHud.mount();};document.head.appendChild(s);})()" title="Bookmarklet pinned to v1.0.0">Pinned to v1.0.0</a>
|
|
125
125
|
</div>
|
|
126
126
|
|
|
127
127
|
<div class="footer">
|
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ interface A11yHudInstance {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
declare
|
|
43
|
+
declare const BaseElement: typeof HTMLElement;
|
|
44
|
+
declare class A11yHudElement extends BaseElement {
|
|
44
45
|
static observedAttributes: string[];
|
|
45
46
|
private _shadow;
|
|
46
47
|
private _theme;
|
|
@@ -49,7 +50,7 @@ declare class A11yHudElement extends HTMLElement {
|
|
|
49
50
|
private _autoScan;
|
|
50
51
|
private _debounceMs;
|
|
51
52
|
private _results;
|
|
52
|
-
private
|
|
53
|
+
private _activeScan;
|
|
53
54
|
private _mounted;
|
|
54
55
|
private _observer;
|
|
55
56
|
private _unwatchTheme;
|
|
@@ -75,11 +76,15 @@ declare class A11yHudElement extends HTMLElement {
|
|
|
75
76
|
runScan(): Promise<AxeResults>;
|
|
76
77
|
private _getScopeTarget;
|
|
77
78
|
private _applyResolvedTheme;
|
|
79
|
+
private _restartObserver;
|
|
78
80
|
private _startObserver;
|
|
81
|
+
private _scheduleScan;
|
|
79
82
|
private _runScan;
|
|
83
|
+
private _performScan;
|
|
80
84
|
private _getFilteredViolations;
|
|
81
85
|
private _render;
|
|
82
86
|
private _renderScanning;
|
|
87
|
+
private _renderError;
|
|
83
88
|
private _renderPanel;
|
|
84
89
|
private _renderFilters;
|
|
85
90
|
private _renderBody;
|
package/dist/index.js
CHANGED
|
@@ -276,7 +276,7 @@ async function runScan(target = document.body, runOnly) {
|
|
|
276
276
|
const options = runOnly && runOnly.length > 0 ? { runOnly: { type: "tag", values: runOnly } } : {};
|
|
277
277
|
const run = axe.run(target, options).then((results) => {
|
|
278
278
|
const ignores = listIgnores();
|
|
279
|
-
|
|
279
|
+
const violations = results.violations.map((v) => ({
|
|
280
280
|
...v,
|
|
281
281
|
nodes: v.nodes.filter((node) => {
|
|
282
282
|
const first = node.target[0];
|
|
@@ -296,7 +296,7 @@ async function runScan(target = document.body, runOnly) {
|
|
|
296
296
|
});
|
|
297
297
|
return nodes.length > 0 ? { ...v, nodes } : null;
|
|
298
298
|
}).filter((v) => v !== null);
|
|
299
|
-
return results;
|
|
299
|
+
return { ...results, violations };
|
|
300
300
|
}).finally(() => {
|
|
301
301
|
activeRun = null;
|
|
302
302
|
});
|
|
@@ -381,7 +381,9 @@ function buildStyleSheet(css) {
|
|
|
381
381
|
return null;
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
-
var
|
|
384
|
+
var BaseElement = typeof HTMLElement === "undefined" ? class {
|
|
385
|
+
} : HTMLElement;
|
|
386
|
+
var A11yHudElement = class extends BaseElement {
|
|
385
387
|
static observedAttributes = ["theme", "scope", "auto-scan", "debounce", "run-only"];
|
|
386
388
|
_shadow;
|
|
387
389
|
_theme = "auto";
|
|
@@ -390,7 +392,7 @@ var A11yHudElement = class extends HTMLElement {
|
|
|
390
392
|
_autoScan = true;
|
|
391
393
|
_debounceMs = 500;
|
|
392
394
|
_results;
|
|
393
|
-
|
|
395
|
+
_activeScan;
|
|
394
396
|
_mounted = false;
|
|
395
397
|
_observer;
|
|
396
398
|
_unwatchTheme;
|
|
@@ -407,7 +409,7 @@ var A11yHudElement = class extends HTMLElement {
|
|
|
407
409
|
constructor() {
|
|
408
410
|
super();
|
|
409
411
|
this._shadow = this.attachShadow({ mode: "open" });
|
|
410
|
-
this._debouncedScan = debounce(() =>
|
|
412
|
+
this._debouncedScan = debounce(() => this._scheduleScan(), this._debounceMs);
|
|
411
413
|
this._applyStyles();
|
|
412
414
|
}
|
|
413
415
|
_applyStyles() {
|
|
@@ -433,7 +435,7 @@ ${themes_default}`;
|
|
|
433
435
|
}
|
|
434
436
|
);
|
|
435
437
|
if (this._autoScan) this._startObserver();
|
|
436
|
-
|
|
438
|
+
this._scheduleScan();
|
|
437
439
|
}
|
|
438
440
|
disconnectedCallback() {
|
|
439
441
|
this._mounted = false;
|
|
@@ -454,6 +456,7 @@ ${themes_default}`;
|
|
|
454
456
|
case "scope":
|
|
455
457
|
this._scopeSelector = value ?? void 0;
|
|
456
458
|
this._scopeElement = void 0;
|
|
459
|
+
this._restartObserver();
|
|
457
460
|
break;
|
|
458
461
|
case "auto-scan":
|
|
459
462
|
this._autoScan = value !== null;
|
|
@@ -470,7 +473,7 @@ ${themes_default}`;
|
|
|
470
473
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
471
474
|
this._debounceMs = Number.isNaN(parsed) ? 500 : parsed;
|
|
472
475
|
this._debouncedScan.cancel();
|
|
473
|
-
this._debouncedScan = debounce(() =>
|
|
476
|
+
this._debouncedScan = debounce(() => this._scheduleScan(), this._debounceMs);
|
|
474
477
|
break;
|
|
475
478
|
}
|
|
476
479
|
case "run-only": {
|
|
@@ -489,8 +492,10 @@ ${themes_default}`;
|
|
|
489
492
|
return this._scopeElement;
|
|
490
493
|
}
|
|
491
494
|
set scopeElement(el) {
|
|
495
|
+
const changed = el !== this._scopeElement || this._scopeSelector !== void 0;
|
|
492
496
|
this._scopeElement = el;
|
|
493
497
|
this._scopeSelector = void 0;
|
|
498
|
+
if (changed) this._restartObserver();
|
|
494
499
|
}
|
|
495
500
|
setTheme(theme) {
|
|
496
501
|
this._theme = theme;
|
|
@@ -506,27 +511,33 @@ ${themes_default}`;
|
|
|
506
511
|
}
|
|
507
512
|
this._updateRunOnlyChips();
|
|
508
513
|
}
|
|
509
|
-
|
|
514
|
+
runScan() {
|
|
510
515
|
return this._runScan();
|
|
511
516
|
}
|
|
512
517
|
_getScopeTarget() {
|
|
513
518
|
if (this._scopeElement) return this._scopeElement;
|
|
514
519
|
if (this._scopeSelector) {
|
|
515
|
-
|
|
520
|
+
try {
|
|
521
|
+
return document.querySelector(this._scopeSelector) ?? document.body;
|
|
522
|
+
} catch {
|
|
523
|
+
return document.body;
|
|
524
|
+
}
|
|
516
525
|
}
|
|
517
526
|
return document.body;
|
|
518
527
|
}
|
|
519
528
|
_applyResolvedTheme() {
|
|
520
529
|
this.dataset.theme = resolveTheme(this._theme);
|
|
521
530
|
}
|
|
531
|
+
_restartObserver() {
|
|
532
|
+
if (this._mounted && this._autoScan) this._startObserver();
|
|
533
|
+
}
|
|
522
534
|
_startObserver() {
|
|
523
535
|
this._observer?.disconnect();
|
|
524
536
|
const target = this._getScopeTarget();
|
|
525
|
-
const debouncedScan = this._debouncedScan;
|
|
526
537
|
this._observer = new MutationObserver((records) => {
|
|
527
538
|
if (records.every((r) => r.type === "attributes" && r.attributeName === HIGHLIGHT_ATTR))
|
|
528
539
|
return;
|
|
529
|
-
|
|
540
|
+
this._debouncedScan();
|
|
530
541
|
});
|
|
531
542
|
this._observer.observe(target, {
|
|
532
543
|
childList: true,
|
|
@@ -535,9 +546,20 @@ ${themes_default}`;
|
|
|
535
546
|
characterData: false
|
|
536
547
|
});
|
|
537
548
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
549
|
+
// Fire-and-forget entry point for internal triggers (observer, buttons).
|
|
550
|
+
// Errors are surfaced in the panel by _runScan, so swallow the rejection here.
|
|
551
|
+
_scheduleScan() {
|
|
552
|
+
this._runScan().catch(() => {
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
_runScan() {
|
|
556
|
+
if (this._activeScan) return this._activeScan;
|
|
557
|
+
this._activeScan = this._performScan().finally(() => {
|
|
558
|
+
this._activeScan = void 0;
|
|
559
|
+
});
|
|
560
|
+
return this._activeScan;
|
|
561
|
+
}
|
|
562
|
+
async _performScan() {
|
|
541
563
|
const rescanBtn = this._shadow.querySelector("#btn-rescan");
|
|
542
564
|
if (rescanBtn) {
|
|
543
565
|
rescanBtn.setAttribute("data-scanning", "");
|
|
@@ -552,8 +574,10 @@ ${themes_default}`;
|
|
|
552
574
|
this._results = results;
|
|
553
575
|
this._render();
|
|
554
576
|
return results;
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (this._mounted) this._renderError(error);
|
|
579
|
+
throw error;
|
|
555
580
|
} finally {
|
|
556
|
-
this._scanning = false;
|
|
557
581
|
const btn = this._shadow.querySelector("#btn-rescan");
|
|
558
582
|
if (btn) {
|
|
559
583
|
btn.removeAttribute("data-scanning");
|
|
@@ -612,6 +636,20 @@ ${themes_default}`;
|
|
|
612
636
|
`;
|
|
613
637
|
}
|
|
614
638
|
}
|
|
639
|
+
_renderError(error) {
|
|
640
|
+
if (this._keyboardMode) return;
|
|
641
|
+
const body = this._shadow.querySelector(".panel-body");
|
|
642
|
+
if (!body) return;
|
|
643
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
644
|
+
body.innerHTML = `
|
|
645
|
+
<div class="empty-state" role="alert">
|
|
646
|
+
<span class="empty-state-icon" aria-hidden="true">${icon("alert-triangle")}</span>
|
|
647
|
+
<span class="empty-state-title">Scan failed</span>
|
|
648
|
+
<span class="empty-state-body">${escapeHtml(message)}</span>
|
|
649
|
+
</div>
|
|
650
|
+
${this._renderIgnoredSection()}
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
615
653
|
_renderPanel(violations, total) {
|
|
616
654
|
return `
|
|
617
655
|
<div class="panel" role="complementary" aria-label="Accessibility panel">
|
|
@@ -741,7 +779,7 @@ ${themes_default}`;
|
|
|
741
779
|
const nodeCount = violation.nodes.length;
|
|
742
780
|
const nodeLabel = `${nodeCount} node${nodeCount !== 1 ? "s" : ""}`;
|
|
743
781
|
const nodes = violation.nodes.map((node, ni) => {
|
|
744
|
-
const selector = this._nodeSelector(node.target);
|
|
782
|
+
const selector = escapeHtml(this._nodeSelector(node.target));
|
|
745
783
|
return `
|
|
746
784
|
<li class="violation-node-item">
|
|
747
785
|
<button class="btn-highlight" data-violation="${index}" data-node="${ni}" aria-label="Highlight: ${selector}">
|
|
@@ -784,7 +822,7 @@ ${themes_default}`;
|
|
|
784
822
|
const panel = this._shadow.querySelector(".panel");
|
|
785
823
|
if (!panel) return;
|
|
786
824
|
panel.querySelector(".btn-panel-title")?.addEventListener("click", () => this._toggleFilters());
|
|
787
|
-
panel.querySelector("#btn-rescan")?.addEventListener("click", () =>
|
|
825
|
+
panel.querySelector("#btn-rescan")?.addEventListener("click", () => this._scheduleScan());
|
|
788
826
|
panel.querySelector("#btn-minimize")?.addEventListener("click", () => {
|
|
789
827
|
this.setAttribute("data-minimized", "");
|
|
790
828
|
});
|
|
@@ -850,7 +888,7 @@ ${themes_default}`;
|
|
|
850
888
|
const ruleId = ignoreBtn.dataset.ignoreRule;
|
|
851
889
|
if (ruleId) {
|
|
852
890
|
addIgnore(ruleId);
|
|
853
|
-
|
|
891
|
+
this._scheduleScan();
|
|
854
892
|
}
|
|
855
893
|
return;
|
|
856
894
|
}
|
|
@@ -860,7 +898,7 @@ ${themes_default}`;
|
|
|
860
898
|
const selector = removeIgnoreBtn.dataset.removeSelector || void 0;
|
|
861
899
|
if (ruleId) {
|
|
862
900
|
removeIgnore(ruleId, selector);
|
|
863
|
-
|
|
901
|
+
this._scheduleScan();
|
|
864
902
|
}
|
|
865
903
|
return;
|
|
866
904
|
}
|
|
@@ -874,7 +912,7 @@ ${themes_default}`;
|
|
|
874
912
|
}
|
|
875
913
|
if (target.closest(".btn-clear-ignores")) {
|
|
876
914
|
clearIgnores();
|
|
877
|
-
|
|
915
|
+
this._scheduleScan();
|
|
878
916
|
return;
|
|
879
917
|
}
|
|
880
918
|
const ignoredToggle = target.closest(".ignored-section-toggle");
|
|
@@ -921,7 +959,7 @@ ${themes_default}`;
|
|
|
921
959
|
chip.setAttribute("aria-pressed", "true");
|
|
922
960
|
}
|
|
923
961
|
this._updateFilterToggle();
|
|
924
|
-
|
|
962
|
+
this._scheduleScan();
|
|
925
963
|
}
|
|
926
964
|
}
|
|
927
965
|
}
|
|
@@ -1123,7 +1161,7 @@ ${themes_default}`;
|
|
|
1123
1161
|
const reader = new FileReader();
|
|
1124
1162
|
reader.onload = (evt) => {
|
|
1125
1163
|
importIgnores(evt.target?.result ?? "");
|
|
1126
|
-
|
|
1164
|
+
this._scheduleScan();
|
|
1127
1165
|
};
|
|
1128
1166
|
reader.readAsText(file);
|
|
1129
1167
|
};
|
|
@@ -1179,7 +1217,9 @@ ${themes_default}`;
|
|
|
1179
1217
|
this._removeHighlight = () => el.removeAttribute(HIGHLIGHT_ATTR);
|
|
1180
1218
|
}
|
|
1181
1219
|
};
|
|
1182
|
-
customElements.
|
|
1220
|
+
if (typeof customElements !== "undefined" && !customElements.get("a11y-hud")) {
|
|
1221
|
+
customElements.define("a11y-hud", A11yHudElement);
|
|
1222
|
+
}
|
|
1183
1223
|
|
|
1184
1224
|
// src/mount.ts
|
|
1185
1225
|
function mount(options = {}) {
|