pour-engine 1.35.5 → 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,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