pour-engine 1.35.4 → 1.36.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/config/project.config.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// WCAG SC 1.1.1 Non-text Content (Level A)
|
|
2
2
|
// alt="" (exactly empty) is the CONFORMING decorative marker (H67) — it
|
|
3
3
|
// hides the image from screen readers on purpose and passes. A
|
|
4
|
-
// whitespace-only alt is neither a text alternative nor that marker
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// whitespace-only alt is neither a text alternative nor that marker by the
|
|
5
|
+
// letter of the spec, but the accessible name computes to empty and current
|
|
6
|
+
// screen readers skip the image as if it were alt="", and a single space
|
|
7
|
+
// was once a documented workaround for old JAWS reading the file name. Its
|
|
8
|
+
// authorship is ambiguous rather than a proven barrier, so since 2026-09-01
|
|
9
|
+
// it is flagged for eyes, not failed at critical alongside a missing alt.
|
|
10
|
+
// Filename/generic alts technically satisfy presence but tell the user
|
|
11
|
+
// nothing (F30/F39) — flagged for eyes.
|
|
8
12
|
const FILENAME_ALT = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)([?#].*)?$/i;
|
|
9
13
|
const GENERIC_ALT = /^(image|img|photo|photograph|picture|graphic|icon|untitled|placeholder|spacer|\d+)$/i;
|
|
10
14
|
|
|
@@ -25,9 +29,8 @@ export default {
|
|
|
25
29
|
if (alt === '') return { status: 'pass' };
|
|
26
30
|
if (!alt.trim()) {
|
|
27
31
|
return {
|
|
28
|
-
status: '
|
|
29
|
-
message: `alt="${alt}" is only whitespace —
|
|
30
|
-
fix: 'Use alt="" (nothing between the quotes) for decorative images, or write a real description.',
|
|
32
|
+
status: 'incomplete',
|
|
33
|
+
message: `alt="${alt}" is only whitespace — not a description, and not the decorative marker either (that is alt="" with nothing between the quotes). Screen readers skip it today, so check the image is really decorative; if it carries meaning, describe it, and either way use alt="".`,
|
|
31
34
|
};
|
|
32
35
|
}
|
|
33
36
|
const trimmed = alt.trim();
|
|
@@ -1,13 +1,65 @@
|
|
|
1
1
|
// WCAG SC 4.1.2 Name, Role, Value (Level A)
|
|
2
|
-
// Roles that are meaningless without a state
|
|
2
|
+
// Roles that are meaningless without a state or property alongside them.
|
|
3
|
+
// The table is ARIA 1.2's "Required States and Properties" row for every
|
|
4
|
+
// role that has one, read from the spec on 2026-09-01: checkbox, switch,
|
|
5
|
+
// radio, menuitemcheckbox and menuitemradio need aria-checked; slider,
|
|
6
|
+
// meter and scrollbar need aria-valuenow (scrollbar also aria-controls);
|
|
7
|
+
// a SEPARATOR needs aria-valuenow only when it is focusable, because a
|
|
8
|
+
// focusable separator is a resizing widget and a static one is a divider;
|
|
9
|
+
// heading needs aria-level; combobox needs aria-expanded and aria-controls;
|
|
10
|
+
// option needs aria-selected.
|
|
11
|
+
//
|
|
12
|
+
// Two of those properties are held at review rather than failed. Browsers
|
|
13
|
+
// and screen readers get by without aria-controls (the relationship it
|
|
14
|
+
// names is rarely announced, and a collapsed combobox has nothing to
|
|
15
|
+
// control yet), and without aria-selected on an option (selection is
|
|
16
|
+
// often conveyed by focus alone); the spec lists both, the DOM cannot
|
|
17
|
+
// show a user is blocked, so the finding asks rather than asserts.
|
|
3
18
|
const REQUIRED = {
|
|
4
|
-
checkbox: 'aria-checked',
|
|
5
|
-
switch: 'aria-checked',
|
|
6
|
-
radio: 'aria-checked',
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
19
|
+
checkbox: ['aria-checked'],
|
|
20
|
+
switch: ['aria-checked'],
|
|
21
|
+
radio: ['aria-checked'],
|
|
22
|
+
menuitemcheckbox: ['aria-checked'],
|
|
23
|
+
menuitemradio: ['aria-checked'],
|
|
24
|
+
slider: ['aria-valuenow'],
|
|
25
|
+
meter: ['aria-valuenow'],
|
|
26
|
+
scrollbar: ['aria-valuenow', 'aria-controls'],
|
|
27
|
+
separator: ['aria-valuenow'],
|
|
28
|
+
heading: ['aria-level'],
|
|
29
|
+
combobox: ['aria-expanded', 'aria-controls'],
|
|
30
|
+
option: ['aria-selected'],
|
|
10
31
|
};
|
|
32
|
+
const HELD_AT_REVIEW = new Set(['aria-controls', 'aria-selected']);
|
|
33
|
+
|
|
34
|
+
// What each missing property costs the user, in the message.
|
|
35
|
+
const COST = {
|
|
36
|
+
'aria-checked': "screen readers can't announce whether it is on or off",
|
|
37
|
+
'aria-valuenow': "screen readers can't announce its value or position",
|
|
38
|
+
'aria-level': 'browsers assume level 2 for a heading with no level, so the outline a screen reader user navigates by is a guess',
|
|
39
|
+
'aria-expanded': "screen readers can't announce whether its list is open",
|
|
40
|
+
'aria-controls': 'assistive technology has no link from the control to what it operates',
|
|
41
|
+
'aria-selected': 'a screen reader may not announce which option is selected',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Native semantics supply the state regardless of WHICH state-bearing
|
|
45
|
+
// role is claimed: HTML-AAM maps an input's checkedness to aria-checked
|
|
46
|
+
// (input type=checkbox role="switch" — the standard toggle pattern —
|
|
47
|
+
// exposes checked=false/true in the browser's tree with no ARIA at all),
|
|
48
|
+
// a range input's or meter's value to aria-valuenow, a heading element's
|
|
49
|
+
// rank to aria-level, an option's selectedness to aria-selected, and a
|
|
50
|
+
// select's open state to aria-expanded.
|
|
51
|
+
function suppliedNatively(element, attr) {
|
|
52
|
+
const tag = element.tagName;
|
|
53
|
+
const type = tag === 'INPUT' ? element.type : null;
|
|
54
|
+
switch (attr) {
|
|
55
|
+
case 'aria-checked': return type === 'checkbox' || type === 'radio';
|
|
56
|
+
case 'aria-valuenow': return type === 'range' || tag === 'METER' || tag === 'PROGRESS';
|
|
57
|
+
case 'aria-level': return /^H[1-6]$/.test(tag);
|
|
58
|
+
case 'aria-selected': return tag === 'OPTION';
|
|
59
|
+
case 'aria-expanded': return tag === 'SELECT';
|
|
60
|
+
default: return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
11
63
|
|
|
12
64
|
export default {
|
|
13
65
|
id: 'role-required-aria',
|
|
@@ -19,23 +71,31 @@ export default {
|
|
|
19
71
|
selector: Object.keys(REQUIRED).map((role) => `[role="${role}"]`).join(', '),
|
|
20
72
|
evaluate(element) {
|
|
21
73
|
const role = element.getAttribute('role');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
74
|
+
// A separator that cannot take focus is a static divider (an <hr> by
|
|
75
|
+
// another name) and requires nothing; only the focusable, moveable
|
|
76
|
+
// kind is a widget with a position to announce.
|
|
77
|
+
if (role === 'separator' && !(element.tabIndex >= 0)) return { status: 'pass' };
|
|
78
|
+
const missing = REQUIRED[role].filter((attr) => !element.hasAttribute(attr) && !suppliedNatively(element, attr));
|
|
79
|
+
if (!missing.length) return { status: 'pass' };
|
|
80
|
+
const failing = missing.filter((attr) => !HELD_AT_REVIEW.has(attr));
|
|
81
|
+
if (failing.length) {
|
|
82
|
+
const list = failing.join(' and ');
|
|
83
|
+
const why = role === 'separator'
|
|
84
|
+
? `this separator takes focus, so it is a resizing widget, but without aria-valuenow ${COST['aria-valuenow']}`
|
|
85
|
+
: failing.map((attr) => COST[attr]).join('; ');
|
|
86
|
+
return {
|
|
87
|
+
status: 'fail',
|
|
88
|
+
message: `role="${role}" without ${list} — ${why}.`,
|
|
89
|
+
fix: `Add ${list} and keep it updated from your script, or use the native HTML element instead.`,
|
|
90
|
+
};
|
|
32
91
|
}
|
|
33
|
-
|
|
34
|
-
|
|
92
|
+
// Only review-held properties are missing. A collapsed combobox has no
|
|
93
|
+
// popup to point aria-controls at, so it is asked only while open.
|
|
94
|
+
if (role === 'combobox' && element.getAttribute('aria-expanded') !== 'true') return { status: 'pass' };
|
|
95
|
+
const list = missing.join(' and ');
|
|
35
96
|
return {
|
|
36
|
-
status: '
|
|
37
|
-
message: `role="${role}" without ${
|
|
38
|
-
fix: `Add ${required} and keep it updated from your script, or use the native HTML element instead.`,
|
|
97
|
+
status: 'incomplete',
|
|
98
|
+
message: `role="${role}" without ${list}, which ARIA lists as required for the role — ${missing.map((attr) => COST[attr]).join('; ')}. Check the control is usable with a screen reader; adding ${list} settles it.`,
|
|
39
99
|
};
|
|
40
100
|
},
|
|
41
101
|
};
|
package/package.json
CHANGED