automatica11y 0.3.2 → 0.4.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 +35 -8
- package/package.json +6 -3
- package/skills/automatica11y-runner/SKILL.md +47 -13
- package/skills/automatica11y-runner/references/fixtures.md +38 -2
- package/src/commands/common.js +5 -2
- package/src/data/README.md +19 -0
- package/src/data/wcag-2.2.json +7557 -0
- package/src/data/wcag-2.2.source.json +6 -0
- package/src/frameworks/index.js +42 -0
- package/src/frameworks/react.js +77 -0
- package/src/frameworks/vue.js +90 -0
- package/src/frameworks/wc.js +55 -0
- package/src/globals.d.ts +2 -0
- package/src/harness/bundle.js +23 -8
- package/src/harness/generate/dialects.js +64 -0
- package/src/harness/generate/index.js +12 -0
- package/src/harness/generate/jsx-recipes.js +224 -0
- package/src/harness/generate/jsx.js +76 -0
- package/src/harness/generate/kit.js +50 -0
- package/src/harness/generate/marking.js +64 -0
- package/src/harness/generate/probe.js +97 -0
- package/src/harness/generate/shared.js +12 -0
- package/src/harness/generate/wc-recipes.js +132 -0
- package/src/harness/npm-install.js +85 -5
- package/src/harness/settle.js +17 -0
- package/src/harness/storybook.js +1 -0
- package/src/harness/url.js +13 -3
- package/src/plan/classify.js +11 -4
- package/src/plan/mapping.js +11 -7
- package/src/plan/resolve-npm.js +15 -9
- package/src/plan/subpath.js +133 -0
- package/src/report/comparison.js +21 -6
- package/src/report/parts.js +60 -8
- package/src/run/audit-npm.js +137 -29
- package/src/run/generate-fixture.js +74 -0
- package/src/run/run-plan.js +24 -4
- package/src/run/summary.js +10 -0
- package/src/schema.js +34 -7
- package/src/tiers/computed/checks.js +197 -0
- package/src/tiers/computed/color.js +48 -0
- package/src/tiers/computed/index.js +25 -0
- package/src/tiers/computed/measure-kit.js +225 -0
- package/src/tiers/conditions/checks.js +283 -0
- package/src/tiers/conditions/index.js +30 -0
- package/src/tiers/conditions/kit.js +133 -0
- package/src/tiers/interactions/archetypes.js +117 -4
- package/src/tiers/interactions/focus-indicator.js +39 -0
- package/src/tiers/interactions/helpers.js +87 -16
- package/src/tiers/interactions/index.js +17 -4
- package/src/tiers/rules/axe.js +4 -3
- package/src/wcag/index.js +83 -0
- package/src/harness/npm-react.js +0 -39
- package/src/harness/npm-wc.js +0 -30
|
@@ -12,6 +12,28 @@ export function installHelpers() {
|
|
|
12
12
|
return uids.get(el);
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const FOCUS_PROPS = ["outlineStyle", "outlineWidth", "outlineColor", "boxShadow", "borderTopStyle", "borderTopColor", "borderTopWidth", "backgroundColor", "color", "textDecorationLine"];
|
|
16
|
+
const snapshotOf = (el) => {
|
|
17
|
+
if (!el) return null;
|
|
18
|
+
const read = (node, pseudo) => {
|
|
19
|
+
const s = getComputedStyle(node, pseudo);
|
|
20
|
+
if (pseudo && (s.content === "none" || s.content === "normal")) return null;
|
|
21
|
+
const box = node.getBoundingClientRect();
|
|
22
|
+
return { ...Object.fromEntries(FOCUS_PROPS.map((key) => [key, s[key]])), rendered: s.display !== "none" && s.visibility !== "hidden" && Number(s.opacity) > 0 && box.width > 0 && box.height > 0 };
|
|
23
|
+
};
|
|
24
|
+
const parts = {};
|
|
25
|
+
const add = (key, node, pseudo) => {
|
|
26
|
+
const part = read(node, pseudo);
|
|
27
|
+
if (part) parts[key] = part;
|
|
28
|
+
};
|
|
29
|
+
add("the element", el);
|
|
30
|
+
add("its ::before", el, "::before");
|
|
31
|
+
add("its ::after", el, "::after");
|
|
32
|
+
[...el.querySelectorAll("*")].slice(0, 30).forEach((child, index) => add(`inner element ${index + 1} (${child.tagName.toLowerCase()})`, child));
|
|
33
|
+
const box = el.getBoundingClientRect();
|
|
34
|
+
return { parts, box: { x: box.x, y: box.y, width: box.width, height: box.height } };
|
|
35
|
+
};
|
|
36
|
+
|
|
15
37
|
/** All matches in the document and in every open shadow root. */
|
|
16
38
|
const queryAllDeep = (selector, root = /** @type {Document | ShadowRoot} */ (document)) => {
|
|
17
39
|
const found = [...root.querySelectorAll(selector)];
|
|
@@ -41,6 +63,21 @@ export function installHelpers() {
|
|
|
41
63
|
const byId = (el, id) => (el.getRootNode().getElementById ? el.getRootNode().getElementById(id) : document.getElementById(id));
|
|
42
64
|
const text = (el) => (el?.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 80);
|
|
43
65
|
|
|
66
|
+
/** The text a person sees in an element, following slots to the text assigned to them. */
|
|
67
|
+
const flatText = (el) => {
|
|
68
|
+
if (!el) return "";
|
|
69
|
+
let out = "";
|
|
70
|
+
const walk = (node) => {
|
|
71
|
+
if (node.nodeType === 3) out += node.nodeValue;
|
|
72
|
+
else if (node.nodeType === 1 && node.localName === "slot") {
|
|
73
|
+
const assigned = node.assignedNodes({ flatten: true });
|
|
74
|
+
(assigned.length ? assigned : [...node.childNodes]).forEach(walk);
|
|
75
|
+
} else if (node.nodeType === 1) node.childNodes.forEach(walk);
|
|
76
|
+
};
|
|
77
|
+
walk(el);
|
|
78
|
+
return out.trim().replace(/\s+/g, " ").slice(0, 80);
|
|
79
|
+
};
|
|
80
|
+
|
|
44
81
|
/** The element ARIA says is active: the focused element, or the descendant it points to. */
|
|
45
82
|
const activeTarget = () => {
|
|
46
83
|
const active = deepActive();
|
|
@@ -49,6 +86,20 @@ export function installHelpers() {
|
|
|
49
86
|
return (pointed && byId(active, pointed)) || active;
|
|
50
87
|
};
|
|
51
88
|
|
|
89
|
+
const LIVE_ROLES = { alert: "assertive", status: "polite", log: "polite", marquee: "off", timer: "off" };
|
|
90
|
+
/** How a live region announces: its aria-live value if it has a valid one, otherwise what its role implies. */
|
|
91
|
+
const politeness = (el) => {
|
|
92
|
+
const set = (el.getAttribute("aria-live") ?? "").trim().toLowerCase();
|
|
93
|
+
if (["off", "polite", "assertive"].includes(set)) return set;
|
|
94
|
+
return LIVE_ROLES[el.getAttribute("role") ?? ""] ?? "off";
|
|
95
|
+
};
|
|
96
|
+
const isLiveElement = (el) => Object.hasOwn(LIVE_ROLES, el.getAttribute("role") ?? "") || el.hasAttribute("aria-live");
|
|
97
|
+
/** The closest element, from `el` upward, that is a live region or has an aria-live attribute. */
|
|
98
|
+
const liveRegionOf = (el) => {
|
|
99
|
+
for (let node = el; node; node = node.assignedSlot || node.parentElement || node.getRootNode?.().host) if (node.nodeType === 1 && isLiveElement(node)) return node;
|
|
100
|
+
return null;
|
|
101
|
+
};
|
|
102
|
+
|
|
52
103
|
window.__a11yClicks = 0;
|
|
53
104
|
document.addEventListener("click", () => (window.__a11yClicks += 1), true);
|
|
54
105
|
|
|
@@ -94,6 +145,33 @@ export function installHelpers() {
|
|
|
94
145
|
visible: visible(el),
|
|
95
146
|
}));
|
|
96
147
|
},
|
|
148
|
+
/** The message (data-a11y-root) and the live region it sits in, as they are right now. */
|
|
149
|
+
live() {
|
|
150
|
+
const root = queryDeep("[data-a11y-root]");
|
|
151
|
+
let region = root ? liveRegionOf(root) : null;
|
|
152
|
+
// A web component can keep the role on a wrapper inside its shadow root, around a slot that shows the message.
|
|
153
|
+
if (!region && root?.shadowRoot) region = [...root.shadowRoot.querySelectorAll("[role], [aria-live]")].find((el) => isLiveElement(el) && el.querySelector("slot")) ?? null;
|
|
154
|
+
const chain = [];
|
|
155
|
+
for (let node = root; node && chain.length < 200; node = node.assignedSlot || node.parentElement || node.getRootNode?.().host) chain.push(uid(node));
|
|
156
|
+
return {
|
|
157
|
+
/** The message and everything above it, so a later check can tell whether the region's element was already in the page. */
|
|
158
|
+
ancestorUids: chain,
|
|
159
|
+
present: Boolean(root),
|
|
160
|
+
visible: visible(root),
|
|
161
|
+
text: flatText(root),
|
|
162
|
+
named: Boolean(root && (root.getAttribute("aria-label") || root.getAttribute("aria-labelledby"))),
|
|
163
|
+
region: region ? { uid: uid(region), role: region.getAttribute("role"), ariaLive: region.getAttribute("aria-live"), politeness: politeness(region), isMessage: region === root } : null,
|
|
164
|
+
regionUids: queryAllDeep("[role], [aria-live]").filter(isLiveElement).map(uid),
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
/** Move focus to the first control inside the message, such as a dismiss button. Returns what it found. */
|
|
168
|
+
focusDismiss() {
|
|
169
|
+
const root = queryDeep("[data-a11y-root]");
|
|
170
|
+
const control = root ? [...root.querySelectorAll('button, a[href], input, select, textarea, [role="button"], [tabindex]:not([tabindex="-1"])')].find(visible) : null;
|
|
171
|
+
if (!control) return { found: false };
|
|
172
|
+
control.focus();
|
|
173
|
+
return { found: true, tag: control.localName, label: text(control) || control.getAttribute("aria-label") || "" };
|
|
174
|
+
},
|
|
97
175
|
/** The name a control gets from its label, in the order browsers use. */
|
|
98
176
|
nameSources() {
|
|
99
177
|
const control = queryDeep("[data-a11y-trigger]");
|
|
@@ -116,26 +194,19 @@ export function installHelpers() {
|
|
|
116
194
|
const live = [...root.querySelectorAll('[role="alert"], [aria-live="assertive"], [aria-live="polite"], [role="status"]')].map(text).filter(Boolean);
|
|
117
195
|
return { invalid: control.getAttribute("aria-invalid") === "true" || control.matches(":invalid"), ariaInvalid: control.getAttribute("aria-invalid") === "true", linked, live };
|
|
118
196
|
},
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const style = Object.fromEntries(pick.map((key) => [key, s[key]]));
|
|
126
|
-
const box = el.getBoundingClientRect();
|
|
127
|
-
return { style, box: { x: box.x, y: box.y, width: box.width, height: box.height } };
|
|
197
|
+
/**
|
|
198
|
+
* The resolved styles that can show a focus indicator, for the focused element, its ::before and ::after,
|
|
199
|
+
* and the elements inside it (a library may draw its ring on a child, such as a ripple).
|
|
200
|
+
*/
|
|
201
|
+
focusSnapshot() {
|
|
202
|
+
return snapshotOf(deepActive());
|
|
128
203
|
},
|
|
129
|
-
/** Remember the focused element, so its
|
|
204
|
+
/** Remember the focused element, so its snapshot can be taken after it loses focus. */
|
|
130
205
|
remember() {
|
|
131
206
|
window.__a11yLast = deepActive();
|
|
132
207
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!el) return null;
|
|
136
|
-
const s = getComputedStyle(el);
|
|
137
|
-
const pick = ["outlineStyle", "outlineWidth", "outlineColor", "outlineOffset", "boxShadow", "borderTopColor", "borderTopWidth", "backgroundColor", "color", "textDecorationLine"];
|
|
138
|
-
return Object.fromEntries(pick.map((key) => [key, s[key]]));
|
|
208
|
+
lastSnapshot() {
|
|
209
|
+
return snapshotOf(window.__a11yLast);
|
|
139
210
|
},
|
|
140
211
|
inputValue() {
|
|
141
212
|
const el = queryDeep("[data-a11y-trigger]");
|
|
@@ -7,7 +7,7 @@ const CHECK_TIMEOUT_MS = 25_000;
|
|
|
7
7
|
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
8
8
|
|
|
9
9
|
/** Everything a check can do, bound to one fresh page. */
|
|
10
|
-
function makeContext(page) {
|
|
10
|
+
export function makeContext(page) {
|
|
11
11
|
const ctx = {
|
|
12
12
|
page,
|
|
13
13
|
trigger: page.locator("[data-a11y-trigger]").first(),
|
|
@@ -66,13 +66,25 @@ function makeContext(page) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** Run one check on a fresh page, so no check inherits another's state. */
|
|
69
|
-
export async function runCheck(browser, url, check, { timeoutMs = CHECK_TIMEOUT_MS } = {}) {
|
|
69
|
+
export async function runCheck(browser, url, check, { timeoutMs = CHECK_TIMEOUT_MS, kits = [installHelpers], needsTrigger = true, waitUntil = /** @type {"load" | "networkidle"} */ ("load") } = {}) {
|
|
70
70
|
/** @type {Awaited<ReturnType<typeof openPage>> | null} */
|
|
71
71
|
let opened = null;
|
|
72
|
+
/** @type {Array<Awaited<ReturnType<typeof openPage>>>} */
|
|
73
|
+
const variants = [];
|
|
74
|
+
const beforeGoto = async (page) => { for (const kit of kits) await page.addInitScript(kit); };
|
|
72
75
|
try {
|
|
73
|
-
opened = await openPage(browser, url, { beforeGoto
|
|
76
|
+
opened = await openPage(browser, url, { beforeGoto, waitUntil });
|
|
74
77
|
const ctx = makeContext(opened.page);
|
|
75
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Open the same page again with other browser settings (reduced motion, dark mode, a narrow window), and get a context for it.
|
|
80
|
+
* It closes with the check.
|
|
81
|
+
*/
|
|
82
|
+
ctx.variant = async (options) => {
|
|
83
|
+
const other = await openPage(browser, url, { ...options, beforeGoto, waitUntil });
|
|
84
|
+
variants.push(other);
|
|
85
|
+
return makeContext(other.page);
|
|
86
|
+
};
|
|
87
|
+
if (needsTrigger) await ctx.trigger.waitFor({ state: "attached", timeout: 3000 });
|
|
76
88
|
const outcome = await Promise.race([
|
|
77
89
|
check.run(ctx),
|
|
78
90
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`The check took longer than ${timeoutMs / 1000} seconds.`)), timeoutMs)),
|
|
@@ -82,6 +94,7 @@ export async function runCheck(browser, url, check, { timeoutMs = CHECK_TIMEOUT_
|
|
|
82
94
|
return { name: check.name, criteria: check.criteria, result: "error", detail: `The check couldn't finish: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}` };
|
|
83
95
|
} finally {
|
|
84
96
|
await opened?.close();
|
|
97
|
+
for (const other of variants) await other.close();
|
|
85
98
|
}
|
|
86
99
|
}
|
|
87
100
|
|
package/src/tiers/rules/axe.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
+
import { criterionFromDigits } from "../../wcag/index.js";
|
|
3
4
|
|
|
4
5
|
const require = createRequire(import.meta.url);
|
|
5
6
|
let axeSource = null;
|
|
@@ -27,10 +28,10 @@ export function axeTags(wcag, level) {
|
|
|
27
28
|
return tags;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/** `wcag143` becomes `1.4.3
|
|
31
|
+
/** `wcag143` becomes `1.4.3`, using the W3C's list of criteria. Returns null for tags that aren't success criteria. */
|
|
31
32
|
function criterionFromTag(tag) {
|
|
32
|
-
const match = /^wcag(\d
|
|
33
|
-
return match ?
|
|
33
|
+
const match = /^wcag(\d{3,4})$/.exec(tag);
|
|
34
|
+
return match ? (criterionFromDigits(match[1])?.num ?? null) : null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
/** An axe target can be nested for iframes and shadow roots. Flatten it to one readable selector. */
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WCAG success criteria, read from the W3C's own published JSON (src/data/wcag-2.2.json, unmodified).
|
|
3
|
+
* Criterion numbers, names, levels, and versions all come from that file. Nothing here is typed in by hand.
|
|
4
|
+
* The links are built from each criterion's `id` and are ours, not the W3C's data.
|
|
5
|
+
*
|
|
6
|
+
* Source: Web Content Accessibility Guidelines (WCAG) 2.2, https://www.w3.org/TR/WCAG22/
|
|
7
|
+
* Terms of use: https://github.com/w3c/wcag/blob/main/11ty/json/README.md
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
const DATA = new URL("../data/wcag-2.2.json", import.meta.url);
|
|
12
|
+
const SOURCE = new URL("../data/wcag-2.2.source.json", import.meta.url);
|
|
13
|
+
|
|
14
|
+
/** @typedef {{ num: string, id: string, handle: string, level: "A" | "AA" | "AAA", versions: string[], url: string, understandingUrl: string }} Criterion */
|
|
15
|
+
|
|
16
|
+
/** @type {Map<string, Criterion> | null} */
|
|
17
|
+
let index = null;
|
|
18
|
+
|
|
19
|
+
function load() {
|
|
20
|
+
if (index) return index;
|
|
21
|
+
const data = JSON.parse(readFileSync(DATA, "utf8"));
|
|
22
|
+
index = new Map();
|
|
23
|
+
for (const principle of data.principles) {
|
|
24
|
+
for (const guideline of principle.guidelines) {
|
|
25
|
+
for (const sc of guideline.successcriteria) {
|
|
26
|
+
index.set(sc.num, {
|
|
27
|
+
num: sc.num,
|
|
28
|
+
id: sc.id,
|
|
29
|
+
handle: sc.handle,
|
|
30
|
+
level: sc.level,
|
|
31
|
+
versions: sc.versions,
|
|
32
|
+
url: `https://www.w3.org/TR/WCAG22/#${sc.id}`,
|
|
33
|
+
understandingUrl: `https://www.w3.org/WAI/WCAG22/Understanding/${sc.id}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return index;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The criterion with this number, such as `1.4.3`, or null when WCAG 2.2 has no such criterion. */
|
|
42
|
+
export function criterion(num) {
|
|
43
|
+
return load().get(num) ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The criterion for a number written without dots, the way axe-core tags it: `1410` is 1.4.10. Null when none matches. */
|
|
47
|
+
export function criterionFromDigits(digits) {
|
|
48
|
+
for (const c of load().values()) if (c.num.replaceAll(".", "") === digits) return c;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every criterion, in the W3C's order. */
|
|
53
|
+
export function allCriteria() {
|
|
54
|
+
return [...load().values()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when the criterion is part of this WCAG version (`2.0`, `2.1`, or `2.2`). */
|
|
58
|
+
export function inVersion(num, version) {
|
|
59
|
+
return criterion(num)?.versions.includes(version) ?? false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** "1.4.3 Contrast (Minimum)", or just the number when the W3C data doesn't know it. */
|
|
63
|
+
export function criterionName(num) {
|
|
64
|
+
const c = criterion(num);
|
|
65
|
+
return c ? `${c.num} ${c.handle}` : num;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** "WCAG 2.4.13 Focus Appearance (level AAA)" for use in a sentence. */
|
|
69
|
+
export function criterionRef(num) {
|
|
70
|
+
const c = criterion(num);
|
|
71
|
+
return c ? `WCAG ${c.num} ${c.handle} (level ${c.level})` : `WCAG ${num}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Where the data came from and when it was downloaded, for the report's attribution line. */
|
|
75
|
+
export function wcagSource() {
|
|
76
|
+
return JSON.parse(readFileSync(SOURCE, "utf8"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The sentence a report prints wherever it shows criterion names. */
|
|
80
|
+
export function wcagAttribution() {
|
|
81
|
+
const { retrieved } = wcagSource();
|
|
82
|
+
return `Criterion names and levels come from the W3C's [WCAG 2.2 JSON](https://www.w3.org/WAI/WCAG22/wcag.json), retrieved ${retrieved}. Source: [Web Content Accessibility Guidelines (WCAG) 2.2](https://www.w3.org/TR/WCAG22/), W3C. Links to each criterion are added by automatica11y.`;
|
|
83
|
+
}
|
package/src/harness/npm-react.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/** React flavor: the entry files and button and link templates that go into a bundle. */
|
|
2
|
-
|
|
3
|
-
/** Mounts a fixture's default export. */
|
|
4
|
-
export const entry = (fixturePath, _pkg) => `import { createElement } from "react";
|
|
5
|
-
import { createRoot } from "react-dom/client";
|
|
6
|
-
import Fixture from ${JSON.stringify(fixturePath)};
|
|
7
|
-
const libA11y = new URLSearchParams(location.search).get("libA11y") === "on";
|
|
8
|
-
createRoot(document.getElementById("root")).render(createElement(Fixture, { libA11y }));
|
|
9
|
-
`;
|
|
10
|
-
|
|
11
|
-
/** Loads the whole package, lists its exports, and records compound parts such as Dialog.Trigger. */
|
|
12
|
-
export const discoverEntry = (pkg) => `import * as lib from ${JSON.stringify(pkg)};
|
|
13
|
-
const out = [];
|
|
14
|
-
for (const name of Object.keys(lib)) {
|
|
15
|
-
const value = lib[name];
|
|
16
|
-
const type = typeof value;
|
|
17
|
-
if (value === null || (type !== "function" && type !== "object")) continue;
|
|
18
|
-
const parts = Object.keys(value).filter((key) => /^[A-Z]/.test(key)).slice(0, 30);
|
|
19
|
-
out.push({ name, type, parts });
|
|
20
|
-
}
|
|
21
|
-
window.__a11yExports = out;
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* A fixture for the simple archetypes, from the export name alone.
|
|
26
|
-
* Compound components (dialog, tabs, menu) can't be guessed, so they need a fixture someone writes.
|
|
27
|
-
*/
|
|
28
|
-
export function template(archetype, pkg, exportName) {
|
|
29
|
-
const body = {
|
|
30
|
-
button: `<Component data-a11y-trigger data-a11y-root type="button">Save</Component>`,
|
|
31
|
-
link: `<Component data-a11y-trigger data-a11y-root href="#top">Read more</Component>`,
|
|
32
|
-
}[archetype];
|
|
33
|
-
if (!body) return null;
|
|
34
|
-
return `import { ${exportName} as Component } from ${JSON.stringify(pkg)};
|
|
35
|
-
export default function Fixture() {
|
|
36
|
-
return ${body};
|
|
37
|
-
}
|
|
38
|
-
`;
|
|
39
|
-
}
|
package/src/harness/npm-wc.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/** Web component flavor: framework-free entries and templates. */
|
|
2
|
-
|
|
3
|
-
/** Loads the package so its custom elements get defined, then calls the fixture's default export, `mount(container)`. */
|
|
4
|
-
export const entry = (fixturePath, pkg) => `import ${JSON.stringify(pkg)};
|
|
5
|
-
import mount from ${JSON.stringify(fixturePath)};
|
|
6
|
-
const libA11y = new URLSearchParams(location.search).get("libA11y") === "on";
|
|
7
|
-
await mount(document.getElementById("root"), { libA11y });
|
|
8
|
-
`;
|
|
9
|
-
|
|
10
|
-
/** Loads the whole package. The page's init script records every custom element the package defines. */
|
|
11
|
-
export const discoverEntry = (pkg) => `import * as lib from ${JSON.stringify(pkg)};
|
|
12
|
-
window.__a11yExports = Object.keys(lib).map((name) => ({ name, type: typeof lib[name], parts: [] }));
|
|
13
|
-
`;
|
|
14
|
-
|
|
15
|
-
/** A fixture for the simple archetypes, from the tag name alone. */
|
|
16
|
-
export function template(archetype, tag) {
|
|
17
|
-
const attrs = {
|
|
18
|
-
button: `el.textContent = "Save";`,
|
|
19
|
-
link: `el.setAttribute("href", "#top");\n el.textContent = "Read more";`,
|
|
20
|
-
}[archetype];
|
|
21
|
-
if (!attrs) return null;
|
|
22
|
-
return `export default function mount(container) {
|
|
23
|
-
const el = document.createElement(${JSON.stringify(tag)});
|
|
24
|
-
el.setAttribute("data-a11y-trigger", "");
|
|
25
|
-
el.setAttribute("data-a11y-root", "");
|
|
26
|
-
${attrs}
|
|
27
|
-
container.append(el);
|
|
28
|
-
}
|
|
29
|
-
`;
|
|
30
|
-
}
|