pour-engine 1.36.0 → 1.37.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 +1 -1
- package/engine/rules/wcag/1.3.1-aria-required-children.js +6 -1
- package/engine/rules/wcag/1.3.1-aria-required-parent.js +8 -2
- package/engine/rules/wcag/1.3.1-form-label.js +17 -0
- package/engine/rules/wcag/1.3.1-table-headers.js +7 -1
- package/engine/rules/wcag/4.1.2-aria-label-misuse.js +14 -3
- package/engine/rules/wcag/4.1.2-role-required-aria.js +2 -2
- package/engine/rules/wcag/4.1.2-valid-role.js +1 -1
- package/package.json +1 -1
package/config/project.config.js
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// Composite roles are meaningless without their required child roles —
|
|
3
3
|
// role="list" with no listitems announces as an empty list.
|
|
4
4
|
import { implicitRole } from '../../lib/roles.js';
|
|
5
|
-
|
|
5
|
+
// Every row of ARIA 1.2's Required Owned Elements table; verify:aria-tables
|
|
6
|
+
// diffs this map against the spec (2026-09-01, after feed and row were
|
|
7
|
+
// found missing).
|
|
8
|
+
export const REQUIRED_CHILDREN = {
|
|
9
|
+
feed: ['article'],
|
|
10
|
+
row: ['cell', 'columnheader', 'gridcell', 'rowheader'],
|
|
6
11
|
list: ['listitem'],
|
|
7
12
|
listbox: ['option', 'group'],
|
|
8
13
|
menu: ['menuitem', 'menuitemcheckbox', 'menuitemradio', 'group'],
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
// > group > treeitem exactly as authored). For the roles whose context does
|
|
14
14
|
// not list group (listitem, tab, row, cell), group stays transparent as
|
|
15
15
|
// before.
|
|
16
|
-
|
|
16
|
+
// Every row of ARIA 1.2's Required Context Role table; verify:aria-tables
|
|
17
|
+
// diffs this map against the spec (2026-09-01, after caption, columnheader
|
|
18
|
+
// and rowheader were found missing).
|
|
19
|
+
export const REQUIRED_PARENT = {
|
|
20
|
+
caption: ['figure', 'grid', 'table', 'treegrid'],
|
|
21
|
+
columnheader: ['row'],
|
|
22
|
+
rowheader: ['row'],
|
|
17
23
|
listitem: ['list'],
|
|
18
24
|
option: ['listbox', 'group'],
|
|
19
25
|
menuitem: ['menu', 'menubar', 'group'],
|
|
@@ -27,7 +33,7 @@ const REQUIRED_PARENT = {
|
|
|
27
33
|
gridcell: ['row'],
|
|
28
34
|
};
|
|
29
35
|
|
|
30
|
-
const IMPLICIT_CONTAINER = { ul: 'list', ol: 'list', menu: 'list', table: 'table', tbody: 'rowgroup', thead: 'rowgroup', tfoot: 'rowgroup', tr: 'row' };
|
|
36
|
+
const IMPLICIT_CONTAINER = { ul: 'list', ol: 'list', menu: 'list', table: 'table', tbody: 'rowgroup', thead: 'rowgroup', tfoot: 'rowgroup', tr: 'row', figure: 'figure' };
|
|
31
37
|
|
|
32
38
|
export default {
|
|
33
39
|
id: 'aria-required-parent',
|
|
@@ -125,6 +125,23 @@ export default {
|
|
|
125
125
|
: 'Put visible text inside the label.',
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
// A label points at this id, but the id is shared: for binds to the
|
|
129
|
+
// FIRST element with it, so this later twin has no label and no name
|
|
130
|
+
// even though a label sits beside it (hsbc.com download basket, two
|
|
131
|
+
// rows reusing one document id, 2026-09-01). The fix is a unique id,
|
|
132
|
+
// not another label.
|
|
133
|
+
if (element.id) {
|
|
134
|
+
const root = element.getRootNode();
|
|
135
|
+
const pointed = root.querySelector?.(`label[for="${CSS.escape(element.id)}"]`);
|
|
136
|
+
const first = root.getElementById?.(element.id);
|
|
137
|
+
if (pointed && first && first !== element) {
|
|
138
|
+
return {
|
|
139
|
+
status: 'fail',
|
|
140
|
+
message: `A <label for="${element.id}"> exists, but an earlier element on the page has the same id, so the label belongs to that one and this field has no label at all.`,
|
|
141
|
+
fix: `Give this field a unique id and point its label at that; ids must not repeat on a page.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
128
145
|
return {
|
|
129
146
|
status: 'fail',
|
|
130
147
|
message: 'This form field has no label — users cannot tell what to enter.',
|
|
@@ -84,10 +84,16 @@ export default {
|
|
|
84
84
|
message: 'This table has no header cells and its cells hold block content (headings, paragraphs, lists or forms), which is a layout-table signature. If it really presents data, mark its header cells with <th>; if it is layout scaffolding, add role="presentation".',
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
// A two-column table is a list of label-and-value pairs; its first
|
|
88
|
+
// column is the row headers, and that is the whole fix (gov.uk
|
|
89
|
+
// inheritance tax guidance, 2026-09-01: five such tables, all td).
|
|
90
|
+
const twoColumns = Math.max(0, ...rows.map((row) => row.cells.length)) === 2;
|
|
87
91
|
return {
|
|
88
92
|
status: 'fail',
|
|
89
93
|
message: 'This looks like a data table but has no header cells — screen reader users get the data with no way to tell what each row/column means.',
|
|
90
|
-
fix:
|
|
94
|
+
fix: twoColumns
|
|
95
|
+
? 'Mark the first cell of each row as <th scope="row">: in a two-column table the first column names what the second holds.'
|
|
96
|
+
: 'Mark header cells with <th> (add scope="col" or scope="row" when the table has both).',
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
99
|
return { status: 'pass' };
|
|
@@ -40,6 +40,18 @@ const NAME_FROM_CONTENT = 'a[href], button, summary, h1, h2, h3, h4, h5, h6, th,
|
|
|
40
40
|
+ ' [role="checkbox"], [role="radio"], [role="switch"], [role="heading"],'
|
|
41
41
|
+ ' [role="cell"], [role="gridcell"], [role="columnheader"], [role="rowheader"], [role="tooltip"]';
|
|
42
42
|
|
|
43
|
+
// ARIA 1.2's ten roles that prohibit naming, and the HTML elements that
|
|
44
|
+
// carry them implicitly (HTML-AAM); verify:aria-tables diffs the set
|
|
45
|
+
// against the spec. The rule started with div, span and p alone
|
|
46
|
+
// (generic and paragraph); the other eight, explicit or as strong, em,
|
|
47
|
+
// code, del, s, ins, sub and sup, joined 2026-09-01.
|
|
48
|
+
export const NAME_PROHIBITED_ROLES = ['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'strong', 'subscript', 'superscript'];
|
|
49
|
+
const IMPLICITLY_PROHIBITED = ['div', 'span', 'p', 'strong', 'em', 'code', 'del', 's', 'ins', 'sub', 'sup'];
|
|
50
|
+
const SELECTOR = ['aria-label', 'aria-labelledby'].flatMap((attr) => [
|
|
51
|
+
...IMPLICITLY_PROHIBITED.map((tag) => `${tag}[${attr}]:not([role]):not([tabindex])`),
|
|
52
|
+
...NAME_PROHIBITED_ROLES.map((role) => `[role="${role}"][${attr}]:not([tabindex])`),
|
|
53
|
+
]).join(', ');
|
|
54
|
+
|
|
43
55
|
export default {
|
|
44
56
|
id: 'aria-label-misuse',
|
|
45
57
|
name: 'aria-label placement',
|
|
@@ -50,8 +62,7 @@ export default {
|
|
|
50
62
|
// §5.2.8.6 prohibits BOTH naming attributes on generic — aria-labelledby
|
|
51
63
|
// pointing at perfectly real text is the same authoring error as an inline
|
|
52
64
|
// aria-label, and the more tempting one (a card labelled by its own title).
|
|
53
|
-
selector:
|
|
54
|
-
+ ' div[aria-labelledby]:not([role]):not([tabindex]), span[aria-labelledby]:not([role]):not([tabindex]), p[aria-labelledby]:not([role]):not([tabindex])',
|
|
65
|
+
selector: SELECTOR,
|
|
55
66
|
evaluate(element) {
|
|
56
67
|
// Inside a name-from-content ancestor the label is NOT ignored: accname
|
|
57
68
|
// applies the whole algorithm to each descendant while building the
|
|
@@ -72,7 +83,7 @@ export default {
|
|
|
72
83
|
})();
|
|
73
84
|
return {
|
|
74
85
|
status: 'incomplete',
|
|
75
|
-
message: `${attempt} on a plain <${element.tagName.toLowerCase()}
|
|
86
|
+
message: `${attempt} on ${element.hasAttribute('role') ? `role="${element.getAttribute('role').trim()}"` : `a plain <${element.tagName.toLowerCase()}>`} is prohibited by ARIA: this element's role does not support naming, so although many browser and screen reader pairings announce the label today, that support is not guaranteed anywhere and can differ between assistive technologies. Where it is announced it replaces the element's visible text; where it is not, users get the visible text alone. Check both presentations read correctly, and that nothing here relies on the label being heard. Prohibited markup is not by itself a WCAG failure: the criterion that policed validity, 4.1.1 Parsing, was removed in WCAG 2.2 (w3.org/WAI/WCAG22/Understanding/parsing.html), so what decides this is what users actually hear.`,
|
|
76
87
|
fix: 'If this element must reliably announce something of its own, give it the role it is playing (a landmark, group, or widget role) so the name is legal and exposure is guaranteed — or move the spoken text into visible or visually-hidden real text. If the label is stray, remove it. Do not add a role solely to legalise the label.',
|
|
77
88
|
};
|
|
78
89
|
},
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// control yet), and without aria-selected on an option (selection is
|
|
16
16
|
// often conveyed by focus alone); the spec lists both, the DOM cannot
|
|
17
17
|
// show a user is blocked, so the finding asks rather than asserts.
|
|
18
|
-
const REQUIRED = {
|
|
18
|
+
export const REQUIRED = {
|
|
19
19
|
checkbox: ['aria-checked'],
|
|
20
20
|
switch: ['aria-checked'],
|
|
21
21
|
radio: ['aria-checked'],
|
|
@@ -29,7 +29,7 @@ const REQUIRED = {
|
|
|
29
29
|
combobox: ['aria-expanded', 'aria-controls'],
|
|
30
30
|
option: ['aria-selected'],
|
|
31
31
|
};
|
|
32
|
-
const HELD_AT_REVIEW = new Set(['aria-controls', 'aria-selected']);
|
|
32
|
+
export const HELD_AT_REVIEW = new Set(['aria-controls', 'aria-selected']);
|
|
33
33
|
|
|
34
34
|
// What each missing property costs the user, in the message.
|
|
35
35
|
const COST = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// WCAG SC 4.1.2 Name, Role, Value (Level A)
|
|
2
2
|
import { attributesOf } from '../../lib/dom.js';
|
|
3
3
|
// Concrete, usable roles — abstract roles are invalid in markup.
|
|
4
|
-
const VALID_ROLES = new Set([
|
|
4
|
+
export const VALID_ROLES = new Set([
|
|
5
5
|
'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button',
|
|
6
6
|
'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'complementary',
|
|
7
7
|
'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis',
|
package/package.json
CHANGED