nodality 1.3.0 → 1.3.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/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/bundle.umd.js.LICENSE.txt +1 -1
- package/dist/button.cjs.js.LICENSE.txt +1 -1
- package/dist/button.esm.js.LICENSE.txt +1 -1
- package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/card-getter.esm.js.LICENSE.txt +1 -1
- package/dist/center.cjs.js.LICENSE.txt +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js.LICENSE.txt +1 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js.LICENSE.txt +1 -1
- package/dist/data-list.esm.js.LICENSE.txt +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.cjs.js.LICENSE.txt +1 -1
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +1 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
- package/dist/element-mapper.esm.js +1 -1
- package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/finalresult.esm.js.LICENSE.txt +1 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js.LICENSE.txt +1 -1
- package/dist/free.esm.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
- package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js.LICENSE.txt +1 -1
- package/dist/image.esm.js.LICENSE.txt +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.LICENSE.txt +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +1 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js.LICENSE.txt +1 -1
- package/dist/radio.esm.js.LICENSE.txt +1 -1
- package/dist/range.cjs.js.LICENSE.txt +1 -1
- package/dist/range.esm.js.LICENSE.txt +1 -1
- package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
- package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
- package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +1 -1
- package/dist/stack.esm.js.LICENSE.txt +1 -1
- package/dist/stacker.cjs.js.LICENSE.txt +1 -1
- package/dist/stacker.esm.js.LICENSE.txt +1 -1
- package/dist/table.cjs.js.LICENSE.txt +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js.LICENSE.txt +1 -1
- package/dist/text.esm.js.LICENSE.txt +1 -1
- package/dist/theme.cjs.js.LICENSE.txt +1 -1
- package/dist/theme.esm.js.LICENSE.txt +1 -1
- package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
- package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
- package/dist/ulist.cjs.js.LICENSE.txt +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +1 -1
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/button.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/dropdown-2025.js +1 -1
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +12 -6
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image.js +1 -1
- package/layout/index.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/morph.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +1 -1
- package/layout/side-nav-bar.js +1 -1
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +1 -1
- package/layout/text.js +1 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/agent-surface.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/check-page.js +130 -21
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +1 -1
- package/lib/element-mapper.js +11 -3
- package/lib/element-params.generated.js +2 -2
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +1 -1
- package/lib/normalize-spec.js +1 -1
- package/lib/parse-html.js +1 -1
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +1 -1
- package/lib/raster-presets.js +1 -1
- package/lib/resolve-refs.js +1 -1
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/suggest.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/lib/transition.js +1 -1
- package/lib/validate-nodes.js +52 -1
- package/lib/webmcp-adapter.js +1 -1
- package/package.json +1 -1
- package/schema.json +763 -1
- package/scripts/generate-schema.mjs +9 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.1
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -23,7 +23,13 @@ let label = document.createElement("label");
|
|
|
23
23
|
label.style.position = "relative";
|
|
24
24
|
label.style.display = "block";
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// `createTextNode(undefined)` is the STRING "undefined", and so is
|
|
27
|
+
// `placeholder = undefined`. A field given no title therefore rendered the
|
|
28
|
+
// word "undefined" as its visible label, its placeholder AND its accessible
|
|
29
|
+
// name — a page that says "undefined" to a visitor, with validate_nodes
|
|
30
|
+
// reporting ok. An absent title has to render as absent.
|
|
31
|
+
const title = obj.title == null ? "" : String(obj.title);
|
|
32
|
+
let node = document.createTextNode(title);
|
|
27
33
|
label.appendChild(node);
|
|
28
34
|
label.style.opacity = 0;
|
|
29
35
|
|
|
@@ -39,7 +45,7 @@ var input = document.createElement("input");
|
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
input.style.position = "relative";
|
|
42
|
-
input.placeholder =
|
|
48
|
+
input.placeholder = title;
|
|
43
49
|
|
|
44
50
|
// width:100% plus horizontal padding is 100% PLUS the padding under the
|
|
45
51
|
// default content-box, so a full-width field pushed the page 20px wider
|
|
@@ -50,8 +56,8 @@ var input = document.createElement("input");
|
|
|
50
56
|
// A placeholder is not an accessible name — it disappears the moment
|
|
51
57
|
// anyone types, and assistive technology is not required to announce it.
|
|
52
58
|
// The visible label is right there, so use it.
|
|
53
|
-
if (
|
|
54
|
-
input.setAttribute("aria-label",
|
|
59
|
+
if (title && !input.getAttribute("aria-label")) {
|
|
60
|
+
input.setAttribute("aria-label", title);
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
|
|
@@ -94,7 +100,7 @@ input.addEventListener("focus", () => {
|
|
|
94
100
|
|
|
95
101
|
input.addEventListener("focusout", () => {
|
|
96
102
|
label.style.transform = "scale(1.0)";
|
|
97
|
-
input.placeholder =
|
|
103
|
+
input.placeholder = title;
|
|
98
104
|
label.style.opacity = "0.0";
|
|
99
105
|
});
|
|
100
106
|
|
package/layout/free.js
CHANGED
package/layout/grid-switcher.js
CHANGED
package/layout/grid.js
CHANGED
package/layout/image.js
CHANGED
package/layout/index.js
CHANGED
package/layout/link.js
CHANGED
package/layout/list.js
CHANGED
package/layout/meta-adder.js
CHANGED
package/layout/modal-2025.js
CHANGED
package/layout/morph.js
CHANGED
package/layout/multiswitcher.js
CHANGED
package/layout/nav-bar.js
CHANGED
package/layout/new-nav-bar.js
CHANGED
package/layout/polygon.js
CHANGED
package/layout/prerender-site.js
CHANGED
package/layout/prerender.js
CHANGED
package/layout/progress.js
CHANGED
package/layout/row.js
CHANGED
package/layout/scroll-video.js
CHANGED
package/layout/side-bar.js
CHANGED
package/layout/side-nav-bar.js
CHANGED
package/layout/simple-bar.js
CHANGED
package/layout/slider-2025.js
CHANGED
package/layout/spacer.js
CHANGED
package/layout/stack.js
CHANGED
package/layout/svg.js
CHANGED
package/layout/switcher.js
CHANGED
package/layout/table.js
CHANGED
package/layout/text-field.js
CHANGED
package/layout/text.js
CHANGED
package/layout/ulist.js
CHANGED
package/layout/video.js
CHANGED
package/layout/wrap.js
CHANGED
package/layout/zoom-card.js
CHANGED
package/lib/agent-surface.js
CHANGED
package/lib/card-getter.js
CHANGED
package/lib/check-page.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.1
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -45,18 +45,59 @@ export const DEFAULT_VIEWPORTS = [
|
|
|
45
45
|
*/
|
|
46
46
|
const IN_PAGE = () => {
|
|
47
47
|
const out = [];
|
|
48
|
-
const add = (code, path, got, detail) => out.push({ code, path, got, detail });
|
|
49
48
|
|
|
50
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* A PAGE-UNIQUE selector, so a report can be acted on.
|
|
51
|
+
*
|
|
52
|
+
* The first version was parent-relative and returned `input` for any input
|
|
53
|
+
* that was the only one under its own div. A form with three unlabelled
|
|
54
|
+
* fields produced the string "input" three times, the merge at the bottom
|
|
55
|
+
* of this file keyed on it, and three findings collapsed into one row
|
|
56
|
+
* reading `viewport: "mobile, mobile, mobile"`. The agent was told neither
|
|
57
|
+
* which control nor that there were three — Tier 7 watched a model fail to
|
|
58
|
+
* repair exactly that. A path walks to the nearest id, or to the body.
|
|
59
|
+
*/
|
|
51
60
|
const sel = (el) => {
|
|
52
61
|
if (!el || !el.tagName) return "?";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const seg = (n) => {
|
|
63
|
+
const tag = n.tagName.toLowerCase();
|
|
64
|
+
const sibs = n.parentElement
|
|
65
|
+
? [...n.parentElement.children].filter((x) => x.tagName === n.tagName) : [];
|
|
66
|
+
return sibs.length > 1 ? `${tag}:nth-of-type(${sibs.indexOf(n) + 1})` : tag;
|
|
67
|
+
};
|
|
68
|
+
const parts = [];
|
|
69
|
+
for (let n = el; n && n !== document.body; n = n.parentElement) {
|
|
70
|
+
if (n.id) { parts.unshift(`#${CSS.escape(n.id)}`); break; }
|
|
71
|
+
parts.unshift(seg(n));
|
|
72
|
+
}
|
|
73
|
+
return parts.join(" > ") || el.tagName.toLowerCase();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The SPEC node an element came from, when the page carries annotation.
|
|
78
|
+
* A selector says where in the DOM; this says which descriptor to edit,
|
|
79
|
+
* which is the thing an agent repairing a spec actually needs. Free when
|
|
80
|
+
* the annotation is there, null when it is not — it is opt-in, and the
|
|
81
|
+
* attribute name is spelled out here because this function crosses into
|
|
82
|
+
* the browser as a string and cannot import `SPEC_ATTR`. A test pins the
|
|
83
|
+
* two together.
|
|
84
|
+
*/
|
|
85
|
+
const nod = (el) => {
|
|
86
|
+
for (let n = el; n && n !== document.body; n = n.parentElement) {
|
|
87
|
+
const v = n.getAttribute && n.getAttribute("data-nod");
|
|
88
|
+
if (v) return v;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// `target` is the element itself, so a finding can carry both. A string is
|
|
94
|
+
// passed for findings about the document rather than about an element.
|
|
95
|
+
const add = (code, target, got, detail) => {
|
|
96
|
+
const isEl = !!(target && target.tagName);
|
|
97
|
+
out.push({
|
|
98
|
+
code, path: isEl ? sel(target) : String(target), got, detail,
|
|
99
|
+
nod: isEl ? nod(target) : null,
|
|
100
|
+
});
|
|
60
101
|
};
|
|
61
102
|
|
|
62
103
|
const all = [...document.body.querySelectorAll("*")];
|
|
@@ -78,7 +119,7 @@ const IN_PAGE = () => {
|
|
|
78
119
|
// 2. An element sticking out past the right edge. Left/top are not
|
|
79
120
|
// checked: off-canvas menus and slide-in panels park there legitimately.
|
|
80
121
|
if (r.right > vw + 1 && cs.position !== "fixed") {
|
|
81
|
-
add("ELEMENT_OVERFLOWS_VIEWPORT",
|
|
122
|
+
add("ELEMENT_OVERFLOWS_VIEWPORT", el, Math.round(r.right),
|
|
82
123
|
`extends ${Math.round(r.right - vw)}px past the ${vw}px viewport`);
|
|
83
124
|
}
|
|
84
125
|
|
|
@@ -87,7 +128,7 @@ const IN_PAGE = () => {
|
|
|
87
128
|
if ((cs.overflow === "hidden" || cs.overflowY === "hidden") &&
|
|
88
129
|
el.scrollHeight > el.clientHeight + 2 && el.clientHeight > 0 &&
|
|
89
130
|
(el.textContent || "").trim().length > 0) {
|
|
90
|
-
add("CONTENT_CLIPPED",
|
|
131
|
+
add("CONTENT_CLIPPED", el, el.scrollHeight,
|
|
91
132
|
`content is ${el.scrollHeight}px in a ${el.clientHeight}px box with overflow hidden`);
|
|
92
133
|
}
|
|
93
134
|
|
|
@@ -95,24 +136,24 @@ const IN_PAGE = () => {
|
|
|
95
136
|
// too, which is what this library's cards emit.
|
|
96
137
|
const tag = el.tagName.toLowerCase();
|
|
97
138
|
if (tag === "img" && !el.hasAttribute("alt")) {
|
|
98
|
-
add("IMAGE_WITHOUT_ALT",
|
|
139
|
+
add("IMAGE_WITHOUT_ALT", el, el.getAttribute("src") || "", "no alt attribute");
|
|
99
140
|
}
|
|
100
141
|
if (el.getAttribute("role") === "img" && !el.getAttribute("aria-label")) {
|
|
101
|
-
add("IMAGE_WITHOUT_ALT",
|
|
142
|
+
add("IMAGE_WITHOUT_ALT", el, "role=img", "role=img with no aria-label");
|
|
102
143
|
}
|
|
103
144
|
|
|
104
145
|
// 5. A target too small to hit. 24px is the WCAG 2.2 AA minimum.
|
|
105
146
|
const interactive = tag === "a" || tag === "button" ||
|
|
106
147
|
(tag === "input" && el.type !== "hidden") || el.getAttribute("role") === "button";
|
|
107
148
|
if (interactive && r.width > 0 && (r.width < 24 || r.height < 24)) {
|
|
108
|
-
add("TAP_TARGET_TOO_SMALL",
|
|
149
|
+
add("TAP_TARGET_TOO_SMALL", el, `${Math.round(r.width)}x${Math.round(r.height)}`,
|
|
109
150
|
"below the 24x24 minimum");
|
|
110
151
|
}
|
|
111
152
|
|
|
112
153
|
// 6. A control with nothing to announce.
|
|
113
154
|
if (interactive && !(el.textContent || "").trim() &&
|
|
114
155
|
!el.getAttribute("aria-label") && !el.querySelector("img[alt]:not([alt=''])")) {
|
|
115
|
-
add("CONTROL_WITHOUT_LABEL",
|
|
156
|
+
add("CONTROL_WITHOUT_LABEL", el, tag, "no text and no accessible name");
|
|
116
157
|
}
|
|
117
158
|
}
|
|
118
159
|
|
|
@@ -123,7 +164,7 @@ const IN_PAGE = () => {
|
|
|
123
164
|
for (const h of heads) {
|
|
124
165
|
const lvl = Number(h.tagName[1]);
|
|
125
166
|
if (prev && lvl > prev + 1) {
|
|
126
|
-
add("HEADING_LEVEL_SKIPPED",
|
|
167
|
+
add("HEADING_LEVEL_SKIPPED", h, h.tagName,
|
|
127
168
|
`h${prev} is followed by ${h.tagName.toLowerCase()}`);
|
|
128
169
|
}
|
|
129
170
|
prev = lvl;
|
|
@@ -163,19 +204,83 @@ const IN_PAGE = () => {
|
|
|
163
204
|
const large = size >= 24 || (size >= 18.66 && Number(cs.fontWeight) >= 700);
|
|
164
205
|
const need = large ? 3 : 4.5;
|
|
165
206
|
if (ratio < need) {
|
|
166
|
-
add("LOW_CONTRAST",
|
|
207
|
+
add("LOW_CONTRAST", el, `${ratio.toFixed(2)}:1`,
|
|
167
208
|
`needs ${need}:1 at ${Math.round(size)}px`);
|
|
168
209
|
}
|
|
169
210
|
}
|
|
170
211
|
return out;
|
|
171
212
|
};
|
|
172
213
|
|
|
214
|
+
/**
|
|
215
|
+
* How to repair each finding, in THIS library's vocabulary.
|
|
216
|
+
*
|
|
217
|
+
* `suggestions` and `valid` are in the finding shape because `validate_nodes`
|
|
218
|
+
* puts repairs there, and check_page left both empty on every finding it has
|
|
219
|
+
* ever emitted — a field that is always blank is a field an agent learns to
|
|
220
|
+
* ignore. Tier 7 measured the cost. Told only "54x23, below the 24x24
|
|
221
|
+
* minimum", the strong model guessed `keySet: {key, value}` and was right, the
|
|
222
|
+
* weaker one invented `keySet: {"--tap-target-size": "44px"}` and was wrong,
|
|
223
|
+
* and the page stayed broken through the repair turn. Naming the element was
|
|
224
|
+
* not enough; the report has to name the edit.
|
|
225
|
+
*
|
|
226
|
+
* Kept deliberately short. These are the repair that fits the overwhelming
|
|
227
|
+
* majority of cases, not a treatment of every way a page can be wrong — an
|
|
228
|
+
* agent that needs more has `npx nodality schema <type>`.
|
|
229
|
+
*/
|
|
230
|
+
const REPAIRS = {
|
|
231
|
+
TAP_TARGET_TOO_SMALL: {
|
|
232
|
+
suggestions: [`keySet: { key: "min-height", value: "44px" }`,
|
|
233
|
+
`keySet: { key: "min-width", value: "44px" }`,
|
|
234
|
+
`pad: [{ a: 12 }]`],
|
|
235
|
+
valid: ["at least 24x24 CSS pixels, per WCAG 2.2 AA target size (minimum)"],
|
|
236
|
+
},
|
|
237
|
+
CONTROL_WITHOUT_LABEL: {
|
|
238
|
+
suggestions: [`label: "<what the field is for>" (labelInput)`,
|
|
239
|
+
`text: "<what the control does>" (button, a)`],
|
|
240
|
+
valid: ["every control needs a name assistive technology can announce; " +
|
|
241
|
+
"a placeholder is not one"],
|
|
242
|
+
},
|
|
243
|
+
IMAGE_WITHOUT_ALT: {
|
|
244
|
+
suggestions: [`alt: "<what the image shows>"`,
|
|
245
|
+
`alt: "" if it is decorative and repeats nearby text`],
|
|
246
|
+
valid: ["img takes `alt`; a background image takes it too and becomes aria-label"],
|
|
247
|
+
},
|
|
248
|
+
LOW_CONTRAST: {
|
|
249
|
+
suggestions: [`color: "<a darker or lighter text colour>"`,
|
|
250
|
+
`background: "<a background with more separation>"`],
|
|
251
|
+
valid: ["4.5:1 for body text, 3:1 at 24px or at 18.66px bold"],
|
|
252
|
+
},
|
|
253
|
+
HEADING_LEVEL_SKIPPED: {
|
|
254
|
+
suggestions: [`tag: "h<the next level down>"`],
|
|
255
|
+
valid: ["`tag` sets the heading level independently of the size scale, " +
|
|
256
|
+
"so the outline can be correct without changing the design"],
|
|
257
|
+
},
|
|
258
|
+
ELEMENT_OVERFLOWS_VIEWPORT: {
|
|
259
|
+
suggestions: [`maxWidth: "100%"`, `width: "100%"`, `breakWord: true`],
|
|
260
|
+
valid: ["nothing may extend past the right edge of the viewport"],
|
|
261
|
+
},
|
|
262
|
+
HORIZONTAL_OVERFLOW: {
|
|
263
|
+
suggestions: [`maxWidth: "100%" on whichever element the other findings name`],
|
|
264
|
+
valid: ["the document may not be wider than the viewport"],
|
|
265
|
+
},
|
|
266
|
+
CONTENT_CLIPPED: {
|
|
267
|
+
suggestions: [`height: "auto"`, `overflow: "visible"`],
|
|
268
|
+
valid: ["a box with `overflow: hidden` must be tall enough for its text"],
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
173
272
|
/**
|
|
174
273
|
* Check rendered HTML and return a report in the shape everything else here
|
|
175
274
|
* returns. `html` is a full document or a fragment; a fragment is wrapped.
|
|
176
275
|
*/
|
|
177
276
|
export async function checkPage(html, opts = {}) {
|
|
178
277
|
const viewports = opts.viewports || DEFAULT_VIEWPORTS;
|
|
278
|
+
// `load` is right for a self-contained page, which is what this library
|
|
279
|
+
// emits. A page whose styling arrives over the network — a CDN stylesheet,
|
|
280
|
+
// a webfont — has not been laid out yet at `load`, and measuring it there
|
|
281
|
+
// reports the unstyled document. Callers checking such a page pass
|
|
282
|
+
// "networkidle".
|
|
283
|
+
const waitUntil = opts.waitUntil || "load";
|
|
179
284
|
let chromium;
|
|
180
285
|
try {
|
|
181
286
|
({ chromium } = await import("playwright"));
|
|
@@ -208,16 +313,20 @@ export async function checkPage(html, opts = {}) {
|
|
|
208
313
|
for (const vp of viewports) {
|
|
209
314
|
const page = await browser.newPage({ viewport: { width: vp.width, height: vp.height } });
|
|
210
315
|
try {
|
|
211
|
-
await page.setContent(doc, { waitUntil
|
|
316
|
+
await page.setContent(doc, { waitUntil });
|
|
212
317
|
const found = await page.evaluate(IN_PAGE);
|
|
213
318
|
for (const f of found) {
|
|
214
319
|
errors.push({
|
|
215
320
|
code: f.code,
|
|
216
321
|
path: f.path,
|
|
217
322
|
got: f.got,
|
|
218
|
-
suggestions: [],
|
|
219
|
-
valid: [],
|
|
323
|
+
suggestions: REPAIRS[f.code]?.suggestions ?? [],
|
|
324
|
+
valid: REPAIRS[f.code]?.valid ?? [],
|
|
220
325
|
detail: f.detail,
|
|
326
|
+
// The spec node, when the page was annotated. Null
|
|
327
|
+
// otherwise, rather than absent, so the field's
|
|
328
|
+
// meaning does not depend on whether it is there.
|
|
329
|
+
nod: f.nod ?? null,
|
|
221
330
|
viewport: vp.name,
|
|
222
331
|
});
|
|
223
332
|
}
|