pour-engine 1.42.0 → 1.42.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 +1 -1
- package/engine/COVERAGE.md +1 -1
- package/engine/rules/{wcag/4.1.2-dialog-name.js → best-practice/dialog-name.js} +9 -9
- package/engine/rules/index.js +3 -1
- package/engine/rules/wcag/1.3.4-orientation.js +23 -6
- package/engine/rules/wcag/4.1.2-nested-interactive.js +11 -2
- package/package.json +1 -1
package/config/project.config.js
CHANGED
package/engine/COVERAGE.md
CHANGED
|
@@ -113,7 +113,7 @@ meaning that only a person can settle, even where a rule decides most real pages
|
|
|
113
113
|
|
|
114
114
|
| SC | Name | Level | Coverage | Rules |
|
|
115
115
|
|---|---|---|---|---|
|
|
116
|
-
| 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-allowed-attr, aria-attr-valid, aria-field-name, aria-hidden-focus, aria-label-misuse, aria-valid-refs, button-name, composite-widget-name,
|
|
116
|
+
| 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-allowed-attr, aria-attr-valid, aria-field-name, aria-hidden-focus, aria-label-misuse, aria-valid-refs, button-name, composite-widget-name, frame-title, invoker-target, label-for-valid, nested-interactive, role-required-aria, summary-name, valid-role |
|
|
117
117
|
| 4.1.3 | [Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) | AA | Manual review | — |
|
|
118
118
|
|
|
119
119
|
**Totals:** 51 partially automated · 35 manual — with 98 rules.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// A dialog with no accessible name is announced as just "dialog". ARIA 1.2
|
|
2
|
+
// requires authors to name one, but WCAG 4.1.2 is written for user interface
|
|
3
|
+
// components, defined as controls, and a dialog is a container of controls
|
|
4
|
+
// whose own names 4.1.2 already covers. So the pattern is an ARIA authoring
|
|
5
|
+
// failure, not a WCAG one: demoted from 4.1.2 to best practice on 2026-09-13
|
|
6
|
+
// after rnib.org.uk's unnamed cookie banner was challenged.
|
|
7
7
|
import { labelledByName } from '../../lib/accessible-name.js';
|
|
8
8
|
|
|
9
9
|
export default {
|
|
10
10
|
id: 'dialog-name',
|
|
11
11
|
name: 'Dialog names',
|
|
12
12
|
impact: 'serious',
|
|
13
|
-
tags: ['
|
|
14
|
-
help: 'Dialogs
|
|
15
|
-
helpUrl: 'https://www.w3.org/WAI/
|
|
13
|
+
tags: ['best-practice'],
|
|
14
|
+
help: 'Dialogs should have an accessible name',
|
|
15
|
+
helpUrl: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/',
|
|
16
16
|
// Default visibility filter: a closed <dialog> is display:none, so only
|
|
17
17
|
// dialogs the user can actually encounter are judged.
|
|
18
18
|
selector: 'dialog, [role="dialog"], [role="alertdialog"]',
|
package/engine/rules/index.js
CHANGED
|
@@ -76,7 +76,9 @@ import scrollableRegionFocusable from './wcag/2.1.1-scrollable-region-focusable.
|
|
|
76
76
|
import tableHeaders from './wcag/1.3.1-table-headers.js';
|
|
77
77
|
import contrastEnhanced from './wcag/1.4.6-contrast-enhanced.js';
|
|
78
78
|
import targetSizeEnhanced from './wcag/2.5.5-target-size-enhanced.js';
|
|
79
|
-
|
|
79
|
+
// Demoted to best-practice 2026-09-13: ARIA requires a dialog name, but 4.1.2
|
|
80
|
+
// covers controls and a dialog is their container (see the rule file).
|
|
81
|
+
import dialogName from './best-practice/dialog-name.js';
|
|
80
82
|
import controlContrast from './wcag/1.4.3-control-contrast.js';
|
|
81
83
|
import nonTextContrast from './wcag/1.4.11-non-text-contrast.js';
|
|
82
84
|
import textSpacing from './wcag/1.4.12-text-spacing.js';
|
|
@@ -66,19 +66,19 @@ function quarterTurn(style, win) {
|
|
|
66
66
|
/** Walk the sheet in cascade order, keeping the LAST relevant declaration
|
|
67
67
|
* per root, orientation and property, so a later declaration in the same
|
|
68
68
|
* orientation block undoes an earlier lock. */
|
|
69
|
-
function scanRules(rules, orientationContext, active, state, doc) {
|
|
69
|
+
function scanRules(rules, orientationContext, active, state, doc, query) {
|
|
70
70
|
const win = doc.defaultView;
|
|
71
71
|
for (const rule of rules ?? []) {
|
|
72
72
|
if (rule.type === win.CSSRule.SUPPORTS_RULE) {
|
|
73
73
|
// An unsupported branch cannot apply in either orientation.
|
|
74
|
-
if (
|
|
74
|
+
if (query.supports(rule.conditionText)) scanRules(rule.cssRules, orientationContext, active, state, doc, query);
|
|
75
75
|
continue;
|
|
76
76
|
}
|
|
77
77
|
if (rule.type === win.CSSRule.MEDIA_RULE) {
|
|
78
78
|
const condition = rule.conditionText ?? rule.media.mediaText;
|
|
79
79
|
const orientation = /orientation\s*:\s*(portrait|landscape)/i.exec(condition)?.[1];
|
|
80
80
|
const context = orientation ? { condition, orientation } : orientationContext;
|
|
81
|
-
scanRules(rule.cssRules, context, active &&
|
|
81
|
+
scanRules(rule.cssRules, context, active && query.media(condition), state, doc, query);
|
|
82
82
|
continue;
|
|
83
83
|
}
|
|
84
84
|
if (orientationContext && rule.style) {
|
|
@@ -104,7 +104,7 @@ function scanRules(rules, orientationContext, active, state, doc) {
|
|
|
104
104
|
// Layers and other grouping rules retain the surrounding media state.
|
|
105
105
|
// Unknown conditional groups cannot establish an active restriction.
|
|
106
106
|
if (rule.cssRules) scanRules(rule.cssRules, orientationContext,
|
|
107
|
-
active && rule.conditionText === undefined, state, doc);
|
|
107
|
+
active && rule.conditionText === undefined, state, doc, query);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -121,13 +121,30 @@ export default {
|
|
|
121
121
|
const doc = element.ownerDocument;
|
|
122
122
|
const win = doc.defaultView;
|
|
123
123
|
const state = new Map();
|
|
124
|
+
// Each media and supports condition is asked once per audit. Framework
|
|
125
|
+
// stylesheets repeat a handful of breakpoints thousands of times: ethz.ch
|
|
126
|
+
// carries 10,604 @media blocks over 98 distinct conditions, and asking
|
|
127
|
+
// matchMedia for every block cost this rule about half of its 10 ms there
|
|
128
|
+
// (measured 2026-09-13). The answers cannot change within one pass.
|
|
129
|
+
const mediaAnswers = new Map();
|
|
130
|
+
const supportsAnswers = new Map();
|
|
131
|
+
const query = {
|
|
132
|
+
media: (condition) => {
|
|
133
|
+
if (!mediaAnswers.has(condition)) mediaAnswers.set(condition, win.matchMedia(condition).matches);
|
|
134
|
+
return mediaAnswers.get(condition);
|
|
135
|
+
},
|
|
136
|
+
supports: (condition) => {
|
|
137
|
+
if (!supportsAnswers.has(condition)) supportsAnswers.set(condition, win.CSS.supports(condition));
|
|
138
|
+
return supportsAnswers.get(condition);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
124
141
|
for (const sheet of doc.styleSheets) {
|
|
125
142
|
if (sheet.disabled) continue;
|
|
126
143
|
const media = sheet.media?.mediaText;
|
|
127
|
-
if (media && !
|
|
144
|
+
if (media && !query.media(media)) continue;
|
|
128
145
|
let rules;
|
|
129
146
|
try { rules = sheet.cssRules; } catch { continue; } // cross-origin: unreadable, skip
|
|
130
|
-
scanRules(rules, null, true, state, doc);
|
|
147
|
+
scanRules(rules, null, true, state, doc, query);
|
|
131
148
|
}
|
|
132
149
|
const findings = [];
|
|
133
150
|
for (const [key, entry] of state) {
|
|
@@ -31,7 +31,14 @@ export default {
|
|
|
31
31
|
const candidates = [...element.querySelectorAll(INTERACTIVE)].filter((el) =>
|
|
32
32
|
!el.matches(':disabled') && !isInert(el)
|
|
33
33
|
&& !(el.tagName === 'INPUT' && el.type === 'hidden') && isRendered(el)
|
|
34
|
-
|
|
34
|
+
// aria-hidden content that Tab reaches is aria-hidden-focus's finding.
|
|
35
|
+
// aria-hidden content with a negative tabindex is not, yet a click or a
|
|
36
|
+
// script still moves focus onto it, and ARIA 1.2 says authors SHOULD NOT
|
|
37
|
+
// hide an element that "may receive focus, either directly via
|
|
38
|
+
// interaction with the user or indirectly via programmatic means", so
|
|
39
|
+
// it stays a candidate here.
|
|
40
|
+
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
|
|
41
|
+
&& !(el.closest('[aria-hidden="true"]') && !(el.hasAttribute('tabindex') && el.tabIndex < 0))
|
|
35
42
|
&& (el.matches(NATIVE) || el.hasAttribute('tabindex')));
|
|
36
43
|
// A child requiring review must not conceal a later definite finding.
|
|
37
44
|
const nested = candidates.find((el) => !(el.hasAttribute('tabindex') && el.tabIndex < 0)) ?? candidates[0];
|
|
@@ -45,7 +52,9 @@ export default {
|
|
|
45
52
|
&& !element.matches('a[href], button')) {
|
|
46
53
|
return {
|
|
47
54
|
status: 'incomplete',
|
|
48
|
-
message:
|
|
55
|
+
message: nested.closest('[aria-hidden="true"]')
|
|
56
|
+
? `This control contains an element (<${nested.tagName.toLowerCase()}>) that is hidden from assistive technology but can still receive focus from a click or a script. Focus landing there lands on content a screen reader cannot see. Check whether it can take focus, and if it can, remove it from focus or from aria-hidden.`
|
|
57
|
+
: `This control contains an element (<${nested.tagName.toLowerCase()}>) with a negative tabindex. It can still receive focus. Check that both controls expose the intended name and role, and that focusing and activating the child works correctly.`,
|
|
49
58
|
};
|
|
50
59
|
}
|
|
51
60
|
// <summary> is the one outer control HTML does not forbid this on: its
|
package/package.json
CHANGED