pour-engine 1.34.0 → 1.34.2
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/config/project.config.js
CHANGED
|
@@ -1191,8 +1191,8 @@ export function createContrastRule({ id, tags, help, helpUrl, thresholds }) {
|
|
|
1191
1191
|
// low-vision readers — the one audience the attribute cannot reach. Every
|
|
1192
1192
|
// fail path (solid, image-range, translucent blend, halo) gets the note,
|
|
1193
1193
|
// and both criteria built from this factory inherit it. Adjudicated
|
|
1194
|
-
// 2026-08-22 against a challenged mockup finding
|
|
1195
|
-
//
|
|
1194
|
+
// 2026-08-22 against a challenged mockup finding, and upheld: the
|
|
1195
|
+
// criterion is written for the people who see the text.
|
|
1196
1196
|
const judge = rule.evaluate.bind(rule);
|
|
1197
1197
|
rule.evaluate = async (element, helpers) => {
|
|
1198
1198
|
const verdict = await judge(element, helpers);
|
|
@@ -570,6 +570,15 @@ export function createTargetSizeRule({ id, tags, help, helpUrl, min, spacingExce
|
|
|
570
570
|
// fragment IS its box, so genuine overlays are dismissed exactly
|
|
571
571
|
// as before.
|
|
572
572
|
if (paintedEncloses(other, rects[j], rects[i]) || paintedEncloses(element, rects[i], rects[j])) return false;
|
|
573
|
+
// Crowding across fixed/sticky contexts is scroll state, not
|
|
574
|
+
// layout, exactly as the obscured-target test reads overlap: a
|
|
575
|
+
// consent banner pinned to the bottom of the viewport lays its
|
|
576
|
+
// "Privacy Policy" link on whichever headline happens to sit
|
|
577
|
+
// under it right now, and that headline has clear space the
|
|
578
|
+
// moment the page scrolls. Asserting it as crowded invents a
|
|
579
|
+
// defect the layout does not have (measured live on two news
|
|
580
|
+
// homes carrying the same consent banner, 2026-08-27).
|
|
581
|
+
if (fixedContextOf(other) !== fixedContextOf(element)) return false;
|
|
573
582
|
const within = (box) => {
|
|
574
583
|
if (undersized[j]) {
|
|
575
584
|
const c = { x: box.left + box.width / 2, y: box.top + box.height / 2 };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// WCAG SC 4.1.2 Name, Role, Value (Level A)
|
|
2
2
|
// aria-activedescendant and aria-owns matter most: a dangling
|
|
3
3
|
// activedescendant breaks composite-widget focus reporting entirely.
|
|
4
|
+
import { effectiveRole } from '../../lib/roles.js';
|
|
5
|
+
import { labelledByName } from '../../lib/accessible-name.js';
|
|
6
|
+
|
|
4
7
|
const REF_ATTRIBUTES = [
|
|
5
8
|
'aria-labelledby', 'aria-describedby', 'aria-controls',
|
|
6
9
|
'aria-activedescendant', 'aria-owns', 'aria-errormessage', 'aria-details',
|
|
@@ -17,6 +20,50 @@ const REF_ATTRIBUTES = [
|
|
|
17
20
|
// read-only. The deferred-tooltip pattern is now named in the review
|
|
18
21
|
// message instead, for the human to confirm.
|
|
19
22
|
|
|
23
|
+
// 4.1.2 is scoped to user interface components ("a part of the content
|
|
24
|
+
// that is perceived by users as a single control for a distinct function").
|
|
25
|
+
// A nameless labelledby is only the 4.1.2 failure proper on one of those: a
|
|
26
|
+
// landmark, list or group whose reference died is left unnamed, which WCAG
|
|
27
|
+
// permits for those roles, so the DOM proves an authoring error, not a
|
|
28
|
+
// component without a name. Same reading label-for-valid took on 2026-08-25.
|
|
29
|
+
// Corpus evidence (parity 2026-08-27): the one assert on 137 sites was an
|
|
30
|
+
// empty <aside tabindex="-1"> whose heading never rendered, while identical
|
|
31
|
+
// dead references on two visible <nav>s and a footer list passed only because the name
|
|
32
|
+
// computation falls through to their link text. The verdict must not turn
|
|
33
|
+
// on whether a landmark happens to contain words.
|
|
34
|
+
const WIDGET_ROLES = new Set([
|
|
35
|
+
// ARIA 1.2 widget roles
|
|
36
|
+
'button', 'checkbox', 'gridcell', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio',
|
|
37
|
+
'option', 'progressbar', 'radio', 'scrollbar', 'searchbox', 'separator', 'slider',
|
|
38
|
+
'spinbutton', 'switch', 'tab', 'tabpanel', 'textbox', 'treeitem',
|
|
39
|
+
// ARIA 1.2 composite widget roles
|
|
40
|
+
'combobox', 'grid', 'listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid',
|
|
41
|
+
// window roles: a dialog is operated as one thing, and dialog-name asserts on it anyway
|
|
42
|
+
'dialog', 'alertdialog',
|
|
43
|
+
]);
|
|
44
|
+
const NATIVE_CONTROLS = 'a[href], button, input:not([type="hidden"]), select, textarea, summary, audio[controls], video[controls], [contenteditable="true"]';
|
|
45
|
+
|
|
46
|
+
/** accname step 2D, the host-language naming that every role keeps
|
|
47
|
+
* (name-from-content is the only role-gated step): an image's alt, an
|
|
48
|
+
* svg's own <title>, a table's caption, a fieldset's legend, a figure's
|
|
49
|
+
* figcaption. The Tate footer's social icons are <svg aria-labelledby="title">
|
|
50
|
+
* with a <title>Facebook</title> child and no element with that id: named. */
|
|
51
|
+
function nativeName(element) {
|
|
52
|
+
const tag = element.tagName.toLowerCase();
|
|
53
|
+
if (tag === 'img' || tag === 'area') return element.getAttribute('alt')?.trim() ?? '';
|
|
54
|
+
const child = { svg: 'title', table: 'caption', fieldset: 'legend', figure: 'figcaption' }[tag];
|
|
55
|
+
if (!child) return '';
|
|
56
|
+
return [...element.children].find((c) => c.tagName.toLowerCase() === child)?.textContent.trim() ?? '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isUserInterfaceComponent(element) {
|
|
60
|
+
if (element.matches(NATIVE_CONTROLS)) return true;
|
|
61
|
+
if (WIDGET_ROLES.has(effectiveRole(element))) return true;
|
|
62
|
+
// A tabbable element is operated as a control whatever its role;
|
|
63
|
+
// tabindex="-1" (a skip-link or scroll target) is not.
|
|
64
|
+
return element.tabIndex >= 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
function inspect(element) {
|
|
21
68
|
// ARIA id references cannot cross shadow boundaries: they resolve only
|
|
22
69
|
// within the element's own root (document or shadow tree).
|
|
@@ -48,28 +95,38 @@ function outcome({ missing, ambiguous }, element, accessibleName) {
|
|
|
48
95
|
if (missing.length) {
|
|
49
96
|
// A dangling ref is skipped by the accname computation, so the HARM
|
|
50
97
|
// depends on what remains, and 4.1.2 only cares about name, role and
|
|
51
|
-
// user-settable value (re-decided by David 2026-08-26
|
|
52
|
-
//
|
|
53
|
-
//
|
|
98
|
+
// user-settable value (re-decided by David 2026-08-26 against measured
|
|
99
|
+
// verdicts on the corpus; the old "fragile, validator-flagged"
|
|
100
|
+
// argument was 4.1.1's, and 2.2 retired it):
|
|
54
101
|
// - aria-labelledby that dangles while the element still names
|
|
55
102
|
// itself from elsewhere: the name is fully determinable (accname
|
|
56
103
|
// step 2B processes the valid IDREFs), so it is not a finding here;
|
|
57
|
-
// - aria-labelledby that leaves
|
|
58
|
-
// aria-activedescendant (a focused composite reports the wrong
|
|
59
|
-
// active item): the 4.1.2 failure proper, asserted
|
|
104
|
+
// - aria-labelledby that leaves a USER INTERFACE COMPONENT nameless,
|
|
105
|
+
// and aria-activedescendant (a focused composite reports the wrong
|
|
106
|
+
// active item): the 4.1.2 failure proper, asserted. The same dead
|
|
107
|
+
// reference on a landmark, list or group is asked about (2026-08-27,
|
|
108
|
+
// see isUserInterfaceComponent);
|
|
60
109
|
// - aria-describedby that points nowhere at rest: a description is
|
|
61
110
|
// not name, role or a user-set value, and tooltip libraries create
|
|
62
111
|
// the target on hover or focus, so the DOM proves an authoring
|
|
63
112
|
// error at most. Asked, never asserted;
|
|
64
113
|
// - aria-controls, aria-owns, aria-details, aria-errormessage: the
|
|
65
114
|
// standing policy (asserted; the comparator asserts these too).
|
|
66
|
-
const
|
|
115
|
+
const component = isUserInterfaceComponent(element);
|
|
116
|
+
// The resting name for a component is the full computation (a button
|
|
117
|
+
// names itself from its contents). A landmark, list or group is not
|
|
118
|
+
// allowed name-from-content (accname step 2F), so for those only the
|
|
119
|
+
// surviving labelledby ids, aria-label and title count: otherwise a
|
|
120
|
+
// <nav> full of links would read as named and the dead reference vanish.
|
|
121
|
+
const restingName = component
|
|
122
|
+
? (accessibleName?.(element) ?? '').trim()
|
|
123
|
+
: (labelledByName(element) || element.getAttribute('aria-label') || nativeName(element) || element.getAttribute('title') || '').trim();
|
|
67
124
|
const relevant = missing.filter(({ attr }) => !(attr === 'aria-labelledby' && restingName));
|
|
68
125
|
if (!relevant.length) return { status: 'pass' };
|
|
69
126
|
const line = ({ attr, id }) => attr === 'aria-labelledby'
|
|
70
127
|
? `aria-labelledby="${id}" points to nothing and leaves this element without an accessible name`
|
|
71
128
|
: `${attr}="${id}" points to nothing — assistive technology silently ignores it`;
|
|
72
|
-
const asserted = relevant.filter(({ attr }) => attr !== 'aria-describedby');
|
|
129
|
+
const asserted = relevant.filter(({ attr }) => attr !== 'aria-describedby' && (attr !== 'aria-labelledby' || component));
|
|
73
130
|
if (asserted.length) {
|
|
74
131
|
return {
|
|
75
132
|
status: 'fail',
|
|
@@ -77,6 +134,15 @@ function outcome({ missing, ambiguous }, element, accessibleName) {
|
|
|
77
134
|
fix: 'Correct or remove the reference, or give the target element that id.',
|
|
78
135
|
};
|
|
79
136
|
}
|
|
137
|
+
const unnamed = relevant.filter(({ attr }) => attr === 'aria-labelledby');
|
|
138
|
+
if (unnamed.length) {
|
|
139
|
+
const tag = element.tagName.toLowerCase();
|
|
140
|
+
return {
|
|
141
|
+
status: 'incomplete',
|
|
142
|
+
message: `${unnamed.map(({ id }) => `aria-labelledby="${id}" points to nothing`).join('; ')}, so this <${tag}> has no accessible name. It is not a user interface component, so 4.1.2 does not require one, but the reference was meant to label it: correct it, or remove it if the label was never needed.`,
|
|
143
|
+
fix: 'Correct or remove the reference, or give the target element that id.',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
80
146
|
return {
|
|
81
147
|
status: 'incomplete',
|
|
82
148
|
message: `${relevant.map(line).join('; ')} at rest. The element keeps its name and role, so this is not a proven 4.1.2 failure: if the description is created when the element is hovered or focused (a tooltip library), this works; otherwise correct the reference.`,
|
package/package.json
CHANGED