pour-engine 1.35.1 → 1.35.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
|
@@ -31,17 +31,29 @@ const roleOf = (element) =>
|
|
|
31
31
|
* as empty (2026-08-25 overnight audit; same reasoning as
|
|
32
32
|
* aria-required-parent's flat-tree walk). Closed roots stay invisible, as
|
|
33
33
|
* everywhere else in the engine.
|
|
34
|
+
*
|
|
35
|
+
* The walk also follows <slot>s downward: a container rendered inside a
|
|
36
|
+
* shadow root (a design system's <div role="list"> holding only a <slot>)
|
|
37
|
+
* owns the light-DOM items projected into it, which is what Chromium
|
|
38
|
+
* exposes as list > listitem. Reading the shadow tree alone reported the
|
|
39
|
+
* list as empty (spectrum.adobe.com, 2026-08-27 disagreement crawl).
|
|
34
40
|
*/
|
|
35
41
|
function composedDescendants(element) {
|
|
36
42
|
const found = [];
|
|
37
43
|
const pending = [element];
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const enter = (el) => {
|
|
46
|
+
if (seen.has(el)) return;
|
|
47
|
+
seen.add(el);
|
|
48
|
+
found.push(el);
|
|
49
|
+
if (el.shadowRoot) pending.push(el.shadowRoot);
|
|
50
|
+
if (el.tagName === 'SLOT') for (const assigned of el.assignedElements({ flatten: true })) { enter(assigned); pending.push(assigned); }
|
|
51
|
+
};
|
|
38
52
|
for (let i = 0; i < pending.length; i++) {
|
|
39
53
|
const scope = pending[i];
|
|
40
54
|
if (scope.shadowRoot) pending.push(scope.shadowRoot);
|
|
41
|
-
for (const
|
|
42
|
-
|
|
43
|
-
if (el.shadowRoot) pending.push(el.shadowRoot);
|
|
44
|
-
}
|
|
55
|
+
if (scope.tagName === 'SLOT') for (const assigned of scope.assignedElements({ flatten: true })) { enter(assigned); pending.push(assigned); }
|
|
56
|
+
for (const el of scope.querySelectorAll('*')) enter(el);
|
|
45
57
|
}
|
|
46
58
|
return found;
|
|
47
59
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// WCAG SC 1.3.1 Info and Relationships (Level A)
|
|
2
|
+
import { ROLE_ARIA } from '../../lib/roles.js';
|
|
3
|
+
|
|
2
4
|
export default {
|
|
3
5
|
id: 'listitem-parent',
|
|
4
6
|
name: 'List item placement',
|
|
@@ -28,8 +30,16 @@ export default {
|
|
|
28
30
|
// cannot own listitems and its <li> children are orphaned exactly as if
|
|
29
31
|
// the <ul> were a <div>. Matching the tag alone waved those through
|
|
30
32
|
// (found on ar.wikipedia.org: portal links in <ul role="navigation">).
|
|
33
|
+
//
|
|
34
|
+
// The role that counts is the first token the browser knows. An unknown
|
|
35
|
+
// token (<ol role="breadcrumbs">, seen on a design-system home) is
|
|
36
|
+
// dropped by every user agent (ARIA 1.2 §7.1, fallback roles), so the
|
|
37
|
+
// native list role stands and its <li>s are owned as usual; valid-role
|
|
38
|
+
// reports the misspelling on its own. Reading the raw attribute made
|
|
39
|
+
// the rule blame the list's items for a token that changed nothing.
|
|
31
40
|
const listContainer = (el) => {
|
|
32
|
-
const
|
|
41
|
+
const tokens = (el.getAttribute('role') ?? '').trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
42
|
+
const role = tokens.find((t) => ROLE_ARIA[t]) ?? null;
|
|
33
43
|
if (el.matches('ul, ol, menu')) return !role || ['list', 'presentation', 'none'].includes(role);
|
|
34
44
|
return role === 'list';
|
|
35
45
|
};
|
|
@@ -52,14 +52,25 @@ export default {
|
|
|
52
52
|
// When the element's role is modelled and does not support the attribute
|
|
53
53
|
// at all, the value is moot and aria-allowed-attr already reviews the
|
|
54
54
|
// attribute; reporting the value too would be the same finding twice.
|
|
55
|
+
//
|
|
56
|
+
// An EMPTY value is a third thing. ARIA defines the empty string as the
|
|
57
|
+
// default for aria-current (absent, empty and undefined all mean false),
|
|
58
|
+
// so aria-current="" is a correct way to say "not current" and nothing
|
|
59
|
+
// is lost (a design-system home writes it that way on every non-active
|
|
60
|
+
// link). For the other enumerated states an empty value is one the
|
|
61
|
+
// browser drops, which is the unknown-name situation again: nothing
|
|
62
|
+
// exposed wrongly, possibly a state the author meant. Review, not fail.
|
|
55
63
|
const unknown = [];
|
|
56
64
|
const invalid = [];
|
|
65
|
+
const empty = [];
|
|
57
66
|
let role;
|
|
58
67
|
for (const { name, value } of element.attributes) {
|
|
59
68
|
if (!name.startsWith('aria-')) continue;
|
|
60
69
|
const attr = name.slice(5);
|
|
61
70
|
if (!KNOWN.has(attr)) {
|
|
62
71
|
unknown.push(name);
|
|
72
|
+
} else if (ENUMS[name] && value.trim() === '') {
|
|
73
|
+
if (name !== 'aria-current') empty.push(name);
|
|
63
74
|
} else if (ENUMS[name] && !ENUMS[name].includes(value.trim().toLowerCase())) {
|
|
64
75
|
if (!GLOBAL_ARIA.has(attr)) {
|
|
65
76
|
role ??= effectiveRole(element);
|
|
@@ -75,6 +86,13 @@ export default {
|
|
|
75
86
|
fix: 'Correct the attribute name/value against the ARIA specification, or remove it.',
|
|
76
87
|
};
|
|
77
88
|
}
|
|
89
|
+
if (empty.length) {
|
|
90
|
+
return {
|
|
91
|
+
status: 'incomplete',
|
|
92
|
+
message: `${empty.join(', ')} is empty, so the browser ignores it and no state is exposed. Was a value intended here? If a real state was meant (${empty.map((n) => ENUMS[n].slice(0, 3).join(', ')).join('; ')}, ...), it is missing and this is a 4.1.2 failure; if the attribute is a template leftover, no one is affected.`,
|
|
93
|
+
fix: 'Give the attribute one of its allowed values, or remove it.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
78
96
|
if (unknown.length) {
|
|
79
97
|
return {
|
|
80
98
|
status: 'incomplete',
|
package/package.json
CHANGED