pour-engine 1.7.0 → 1.9.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
package/engine/COVERAGE.md
CHANGED
|
@@ -111,7 +111,7 @@ meaning that only a person can settle, even where a rule decides most real pages
|
|
|
111
111
|
|
|
112
112
|
| SC | Name | Level | Coverage | Rules |
|
|
113
113
|
|---|---|---|---|---|
|
|
114
|
-
| 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) | A | Partially automated | input-image-alt, form-label, link-name, aria-
|
|
114
|
+
| 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) | A | Partially automated | input-image-alt, form-label, link-name, aria-attr-valid, aria-field-name, aria-hidden-focus, aria-valid-refs, button-name, dialog-name, frame-title, label-for-valid, nested-interactive, role-required-aria, valid-role |
|
|
115
115
|
| 4.1.3 | [Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) | AA | Manual review | — |
|
|
116
116
|
|
|
117
117
|
**Totals:** 30 partially automated · 56 manual — with 74 rules.
|
package/engine/lib/roles.js
CHANGED
|
@@ -150,6 +150,15 @@ export function implicitRole(element) {
|
|
|
150
150
|
if (tag === 'img') return element.getAttribute('alt') === '' ? 'presentation' : 'img';
|
|
151
151
|
if (tag === 'header') return element.closest('article, aside, main, nav, section') ? 'generic' : 'banner';
|
|
152
152
|
if (tag === 'footer') return element.closest('article, aside, main, nav, section') ? 'generic' : 'contentinfo';
|
|
153
|
+
// HTML-AAM: an aside inside sectioning content is complementary only when
|
|
154
|
+
// it carries an accessible name; unnamed it is generic. Verified against
|
|
155
|
+
// Chromium's accessibility tree, which demotes exactly that case and keeps
|
|
156
|
+
// a top-level aside complementary whether named or not.
|
|
157
|
+
if (tag === 'aside') {
|
|
158
|
+
const sectioned = element.parentElement?.closest('article, aside, nav, section');
|
|
159
|
+
const named = element.hasAttribute('aria-label') || element.hasAttribute('aria-labelledby');
|
|
160
|
+
return sectioned && !named ? 'generic' : 'complementary';
|
|
161
|
+
}
|
|
153
162
|
if (tag === 'section') {
|
|
154
163
|
return element.hasAttribute('aria-label') || element.hasAttribute('aria-labelledby') ? 'region' : 'generic';
|
|
155
164
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ARIA 1.2 §8.6, repeated verbatim in the 1.3 draft: "authors MUST only use
|
|
2
|
+
// non-global states and properties on elements with a role supporting the
|
|
3
|
+
// state or property". An unsupported attribute breaks that MUST, so this is a
|
|
4
|
+
// real, provable authoring error and the rule asserts it.
|
|
5
|
+
//
|
|
6
|
+
// It is NOT a WCAG failure, which is why it lives here rather than under
|
|
7
|
+
// 4.1.2. An unsupported attribute is INERT, measured in Chromium:
|
|
8
|
+
// aria-expanded on a button exposes expanded=false, while the same attribute
|
|
9
|
+
// on a complementary landmark exposes no state at all. It is dropped, not
|
|
10
|
+
// misreported. So the role is still exposed, the name is still computed, and
|
|
11
|
+
// nothing is announced incorrectly. Name, role and value all survive, which
|
|
12
|
+
// is all 4.1.2 asks for.
|
|
13
|
+
//
|
|
14
|
+
// What CAN fail 4.1.2 is the thing underneath: if the element genuinely has
|
|
15
|
+
// the state the author reached for, that state is now invisible. Whether it
|
|
16
|
+
// does is exactly what the DOM cannot establish, so the message asks rather
|
|
17
|
+
// than asserts. Reporting the ARIA error as a WCAG violation would have been
|
|
18
|
+
// asserting a criterion failure off the back of a question.
|
|
19
|
+
//
|
|
20
|
+
// Note "unsupported" is distinct from "prohibited" (§5.2.5), a separate
|
|
21
|
+
// category about naming that this rule does not cover.
|
|
22
|
+
//
|
|
23
|
+
// Worth knowing when comparing against other tools: aria-expanded WAS
|
|
24
|
+
// inherited into complementary, banner, heading, img and list under ARIA 1.1,
|
|
25
|
+
// and was narrowed in 1.2 (narrowed again in the 1.3 draft). Markup written
|
|
26
|
+
// against 1.1 is not conforming under 1.2, and a tool on the older baseline
|
|
27
|
+
// stays silent on it. Neither reading is a bug; they are different editions.
|
|
28
|
+
import { GLOBAL_ARIA, ROLE_ARIA, KNOWN_ARIA, effectiveRole } from '../../lib/roles.js';
|
|
29
|
+
|
|
30
|
+
// aria-label/labelledby misuse on generic elements has its own rule
|
|
31
|
+
// (aria-label-misuse) — excluded here to avoid double-reporting.
|
|
32
|
+
const HANDLED_ELSEWHERE = new Set(['label', 'labelledby']);
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
id: 'aria-allowed-attr',
|
|
36
|
+
impact: 'moderate',
|
|
37
|
+
tags: ['best-practice'],
|
|
38
|
+
help: 'ARIA attributes must be supported by the element’s role',
|
|
39
|
+
helpUrl: 'https://www.w3.org/TR/wai-aria-1.2/#state_property_processing',
|
|
40
|
+
selector: '*',
|
|
41
|
+
visibleOnly: false,
|
|
42
|
+
evaluate(element) {
|
|
43
|
+
// Plain loop, no intermediate arrays: this runs on every element of the
|
|
44
|
+
// page, and nearly all have no aria-* attributes at all.
|
|
45
|
+
const ariaAttrs = [];
|
|
46
|
+
for (const { name } of element.attributes) {
|
|
47
|
+
if (!name.startsWith('aria-')) continue;
|
|
48
|
+
const attr = name.slice(5);
|
|
49
|
+
// Unknown/misspelled attributes are aria-attr-valid's finding — judging
|
|
50
|
+
// them against a role too would report the same typo twice.
|
|
51
|
+
if (!HANDLED_ELSEWHERE.has(attr) && KNOWN_ARIA.has(attr)) ariaAttrs.push(attr);
|
|
52
|
+
}
|
|
53
|
+
if (!ariaAttrs.length) return { status: 'pass' };
|
|
54
|
+
|
|
55
|
+
const role = effectiveRole(element);
|
|
56
|
+
const allowed = role && ROLE_ARIA[role];
|
|
57
|
+
if (!allowed) return { status: 'pass' }; // role not modelled: don't guess
|
|
58
|
+
|
|
59
|
+
const disallowed = ariaAttrs.filter((name) => !GLOBAL_ARIA.has(name) && !allowed.includes(name));
|
|
60
|
+
if (!disallowed.length) return { status: 'pass' };
|
|
61
|
+
const names = disallowed.map((n) => `aria-${n}`);
|
|
62
|
+
return {
|
|
63
|
+
status: 'fail',
|
|
64
|
+
message: `${names.join(', ')} is not supported on role "${role}", so the browser drops it and assistive technology never sees it. Nothing is announced wrongly, but nothing is announced at all. If this element really does have that state, it is currently invisible to anyone using a screen reader.`,
|
|
65
|
+
fix: `Move ${names.join('/')} to the element whose role supports it, usually the control that toggles this one, or remove it if the element has no such state. Adding a role to this element to make the attribute legal is rarely right: the host language restricts which roles each element may take.`,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
};
|
package/engine/rules/index.js
CHANGED
|
@@ -42,7 +42,10 @@ import audioControl from './wcag/1.4.2-audio-control.js';
|
|
|
42
42
|
import pauseStopHide from './wcag/2.2.2-pause-stop-hide.js';
|
|
43
43
|
import mediaCaptions from './wcag/1.2.2-media-captions.js';
|
|
44
44
|
import ariaAttrValid from './wcag/4.1.2-aria-attr-valid.js';
|
|
45
|
-
|
|
45
|
+
// Demoted to best-practice: an unsupported ARIA attribute breaks an ARIA
|
|
46
|
+
// author MUST (1.2 §8.6) but is INERT, so name, role and value all survive
|
|
47
|
+
// and no success criterion fails (see the note in the rule file).
|
|
48
|
+
import ariaAllowedAttr from './best-practice/aria-allowed-attr.js';
|
|
46
49
|
import ariaFieldName from './wcag/4.1.2-aria-field-name.js';
|
|
47
50
|
// Demoted to best-practice: prohibited-but-inert ARIA on a generic element
|
|
48
51
|
// fails no success criterion (see the note in the rule file).
|
package/package.json
CHANGED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// WCAG SC 4.1.2 Name, Role, Value (Level A)
|
|
2
|
-
// ARIA attributes must be supported by the element's role — aria-selected
|
|
3
|
-
// on a plain link, aria-checked on a heading, etc. are ignored (or worse,
|
|
4
|
-
// misreported) by assistive technology.
|
|
5
|
-
import { GLOBAL_ARIA, ROLE_ARIA, KNOWN_ARIA, effectiveRole } from '../../lib/roles.js';
|
|
6
|
-
|
|
7
|
-
// aria-label/labelledby misuse on generic elements has its own rule
|
|
8
|
-
// (aria-label-misuse) — excluded here to avoid double-reporting.
|
|
9
|
-
const HANDLED_ELSEWHERE = new Set(['label', 'labelledby']);
|
|
10
|
-
|
|
11
|
-
export default {
|
|
12
|
-
id: 'aria-allowed-attr',
|
|
13
|
-
impact: 'critical',
|
|
14
|
-
tags: ['wcag2a', 'wcag412'],
|
|
15
|
-
help: 'ARIA attributes must be allowed for the element’s role',
|
|
16
|
-
helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
|
|
17
|
-
selector: '*',
|
|
18
|
-
visibleOnly: false,
|
|
19
|
-
evaluate(element) {
|
|
20
|
-
// Plain loop, no intermediate arrays: this runs on every element of the
|
|
21
|
-
// page, and nearly all have no aria-* attributes at all.
|
|
22
|
-
const ariaAttrs = [];
|
|
23
|
-
for (const { name } of element.attributes) {
|
|
24
|
-
if (!name.startsWith('aria-')) continue;
|
|
25
|
-
const attr = name.slice(5);
|
|
26
|
-
// Unknown/misspelled attributes are aria-attr-valid's finding — judging
|
|
27
|
-
// them against a role too would report the same typo twice.
|
|
28
|
-
if (!HANDLED_ELSEWHERE.has(attr) && KNOWN_ARIA.has(attr)) ariaAttrs.push(attr);
|
|
29
|
-
}
|
|
30
|
-
if (!ariaAttrs.length) return { status: 'pass' };
|
|
31
|
-
|
|
32
|
-
const role = effectiveRole(element);
|
|
33
|
-
const allowed = role && ROLE_ARIA[role];
|
|
34
|
-
if (!allowed) return { status: 'pass' }; // role not modelled: don't guess
|
|
35
|
-
|
|
36
|
-
const disallowed = ariaAttrs.filter((name) => !GLOBAL_ARIA.has(name) && !allowed.includes(name));
|
|
37
|
-
if (!disallowed.length) return { status: 'pass' };
|
|
38
|
-
return {
|
|
39
|
-
status: 'fail',
|
|
40
|
-
message: `${disallowed.map((n) => `aria-${n}`).join(', ')} is not supported on role "${role}" — assistive technology ignores it or announces the wrong state.`,
|
|
41
|
-
fix: `Use a role that supports ${disallowed.map((n) => `aria-${n}`).join('/')}, or remove the attribute(s).`,
|
|
42
|
-
};
|
|
43
|
-
},
|
|
44
|
-
};
|