pour-engine 1.42.1 → 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.
@@ -6,6 +6,6 @@
6
6
  export default {
7
7
  engine: {
8
8
  name: 'pour engine',
9
- version: '1.42.1',
9
+ version: '1.42.2',
10
10
  },
11
11
  };
@@ -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 (win.CSS.supports(rule.conditionText)) scanRules(rule.cssRules, orientationContext, active, state, doc);
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 && win.matchMedia(condition).matches, state, doc);
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 && !win.matchMedia(media).matches) continue;
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
- && !el.closest('[aria-hidden="true"]')
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: `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.`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.42.1",
3
+ "version": "1.42.2",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",