pour-engine 1.46.0 → 1.46.1
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/lib/contrast.js
CHANGED
|
@@ -145,6 +145,7 @@ let uncoveredEffectCache = new WeakMap();
|
|
|
145
145
|
let blendBackdropCache = new WeakMap();
|
|
146
146
|
let labelReferrersCache = new WeakMap();
|
|
147
147
|
let stackingContextCache = new WeakMap();
|
|
148
|
+
let backgroundRulesCache = new WeakMap();
|
|
148
149
|
const HAS_IMAGE = Symbol('background-image in chain');
|
|
149
150
|
|
|
150
151
|
export function resetAuditCaches() {
|
|
@@ -161,6 +162,7 @@ export function resetAuditCaches() {
|
|
|
161
162
|
blendBackdropCache = new WeakMap();
|
|
162
163
|
labelReferrersCache = new WeakMap();
|
|
163
164
|
stackingContextCache = new WeakMap();
|
|
165
|
+
backgroundRulesCache = new WeakMap();
|
|
164
166
|
}
|
|
165
167
|
|
|
166
168
|
/**
|
|
@@ -981,6 +983,14 @@ export function filmedContrastBounds(foreground, background, film) {
|
|
|
981
983
|
};
|
|
982
984
|
}
|
|
983
985
|
|
|
986
|
+
/** A layer's own opacity. 0 is a real value, not a missing one: consent
|
|
987
|
+
* dimmers commonly fade to opacity 0 and stay in the DOM once dismissed
|
|
988
|
+
* (CSS Color 4, the opacity property: 0 is fully transparent). */
|
|
989
|
+
const opacityOf = (style) => {
|
|
990
|
+
const opacity = parseFloat(style.opacity);
|
|
991
|
+
return Number.isNaN(opacity) ? 1 : opacity;
|
|
992
|
+
};
|
|
993
|
+
|
|
984
994
|
/** A layer qualifies as a scrim over `element` when it is translucent paint
|
|
985
995
|
* (or a backdrop-filter) covering most of the viewport and the whole
|
|
986
996
|
* element — the shape of a modal/loading veil, not a badge or header.
|
|
@@ -994,7 +1004,7 @@ function scrimIn(layersAbove, element, win) {
|
|
|
994
1004
|
if (layer.contains(element) || element.contains(layer)) continue;
|
|
995
1005
|
const style = getComputedStyle(layer);
|
|
996
1006
|
const color = parseColor(style.backgroundColor);
|
|
997
|
-
const alpha = (color ? color.a : 0) * (
|
|
1007
|
+
const alpha = (color ? color.a : 0) * opacityOf(style);
|
|
998
1008
|
const hasBackdropFilter = style.backdropFilter && style.backdropFilter !== 'none';
|
|
999
1009
|
// Fully opaque covers simply hide the text (nothing to judge through);
|
|
1000
1010
|
// near-invisible tints don't meaningfully change presented contrast.
|
|
@@ -1027,7 +1037,7 @@ export function scrimPaint(layers) {
|
|
|
1027
1037
|
if (style.backgroundImage !== 'none') return null;
|
|
1028
1038
|
const color = parseColor(style.backgroundColor);
|
|
1029
1039
|
if (!color) return null;
|
|
1030
|
-
const alpha = color.a * (
|
|
1040
|
+
const alpha = color.a * opacityOf(style);
|
|
1031
1041
|
if (alpha <= 0) continue;
|
|
1032
1042
|
paints.push({ ...color, a: alpha });
|
|
1033
1043
|
}
|
|
@@ -1179,7 +1189,7 @@ export function opaquePanelRects(doc) {
|
|
|
1179
1189
|
// hazard that scrolling cannot escape.
|
|
1180
1190
|
if (!panelRectsCache.veil && (style.position === 'fixed' || style.position === 'sticky')
|
|
1181
1191
|
&& style.visibility !== 'hidden') {
|
|
1182
|
-
const alpha = (color ? color.a : 0) * (
|
|
1192
|
+
const alpha = (color ? color.a : 0) * opacityOf(style);
|
|
1183
1193
|
const hasBackdropFilter = style.backdropFilter && style.backdropFilter !== 'none';
|
|
1184
1194
|
const coversViewport = rect.width >= 0.9 * win.innerWidth && rect.height >= 0.9 * win.innerHeight;
|
|
1185
1195
|
if (coversViewport && (hasBackdropFilter || (alpha >= 0.15 && alpha < 1))) {
|
|
@@ -1988,6 +1998,71 @@ function rootHasFirstLineRules(root) {
|
|
|
1988
1998
|
return has;
|
|
1989
1999
|
}
|
|
1990
2000
|
|
|
2001
|
+
/**
|
|
2002
|
+
* The stylesheet rules behind lazy-loaded backgrounds, per root per audit:
|
|
2003
|
+
* `gates` hold background images back until a loader marks the element
|
|
2004
|
+
* loaded (`background-image: none` on a selector that excludes a lazy-load
|
|
2005
|
+
* class, as in `.section:not(.lazyloaded) { background-image: none }`), and
|
|
2006
|
+
* `images` give an element a background image of its own. Rules under a
|
|
2007
|
+
* media or support condition count only while it holds; an unreadable
|
|
2008
|
+
* cross-origin sheet contributes nothing.
|
|
2009
|
+
*/
|
|
2010
|
+
const LAZY_EXCLUSION = /:not\(\s*\.[\w-]*lazy[\w-]*\s*\)/i;
|
|
2011
|
+
const PAINTS_IMAGE = /url\(|gradient\(|image-set\(|var\(/i;
|
|
2012
|
+
function backgroundRules(root) {
|
|
2013
|
+
let found = backgroundRulesCache.get(root);
|
|
2014
|
+
if (found) return found;
|
|
2015
|
+
found = { gates: [], images: [] };
|
|
2016
|
+
const win = (root.ownerDocument ?? root).defaultView;
|
|
2017
|
+
const scan = (rules) => {
|
|
2018
|
+
for (const rule of rules) {
|
|
2019
|
+
if (rule.styleSheet) {
|
|
2020
|
+
if (rule.media?.mediaText && !win.matchMedia(rule.media.mediaText).matches) continue;
|
|
2021
|
+
try { scan(rule.styleSheet.cssRules); } catch { /* unreadable import */ }
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
if (rule.media) {
|
|
2025
|
+
if (!win.matchMedia(rule.media.mediaText).matches) continue;
|
|
2026
|
+
} else if (win.CSSSupportsRule && rule instanceof win.CSSSupportsRule) {
|
|
2027
|
+
if (!win.CSS.supports(rule.conditionText)) continue;
|
|
2028
|
+
}
|
|
2029
|
+
if (rule.selectorText && rule.style) {
|
|
2030
|
+
const image = rule.style.getPropertyValue('background-image').trim();
|
|
2031
|
+
if (image === 'none' && LAZY_EXCLUSION.test(rule.selectorText)) found.gates.push(rule.selectorText);
|
|
2032
|
+
else if (PAINTS_IMAGE.test(image || rule.style.getPropertyValue('background'))) found.images.push(rule.selectorText);
|
|
2033
|
+
}
|
|
2034
|
+
if (rule.cssRules) scan(rule.cssRules);
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
for (const sheet of [...(root.styleSheets ?? []), ...(root.adoptedStyleSheets ?? [])]) {
|
|
2038
|
+
try { scan(sheet.cssRules); } catch { /* cross-origin */ }
|
|
2039
|
+
}
|
|
2040
|
+
backgroundRulesCache.set(root, found);
|
|
2041
|
+
return found;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const matchesAny = (node, selectors) => selectors.some((selector) => {
|
|
2045
|
+
try { return node.matches(selector); } catch { return false; }
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* Is the background behind this text still to be lazy-loaded? True when an
|
|
2050
|
+
* element on its chain is given a background image by the page's styles, a
|
|
2051
|
+
* lazy-load gate matches it, and it computes no background image now. Page
|
|
2052
|
+
* builders defer section backgrounds this way until the section nears the
|
|
2053
|
+
* viewport, so text the visitor has not scrolled to yet is measured against
|
|
2054
|
+
* a placeholder they never see: the loader restores the image first.
|
|
2055
|
+
*/
|
|
2056
|
+
export function lazyWithheldBackground(element) {
|
|
2057
|
+
for (let node = element; node?.nodeType === 1; node = flatParentOf(node)) {
|
|
2058
|
+
const { gates, images } = backgroundRules(node.getRootNode());
|
|
2059
|
+
if (!gates.length || !matchesAny(node, gates)) continue;
|
|
2060
|
+
if (getComputedStyle(node).backgroundImage !== 'none') continue;
|
|
2061
|
+
if (PAINTS_IMAGE.test(node.style?.backgroundImage ?? '') || matchesAny(node, images)) return true;
|
|
2062
|
+
}
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
1991
2066
|
/**
|
|
1992
2067
|
* Colours that repaint part of this element's own text through ::first-line
|
|
1993
2068
|
* or ::first-letter, when they differ from the colour the element itself
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
opacityAnimating, restingOpacity, mediaRects, inZeroClipSubtree,
|
|
7
7
|
paintedBackdrop, opaquePanelRects, viewportVeil, textShadowHalo, textShadowNegligible,
|
|
8
8
|
pseudoBackdropForText, filmedContrastBounds, backgroundColorSource, scrimPaint, applyOverlays,
|
|
9
|
-
showRatio, asRgb, splitBackgroundLayers, backgroundLayerUrl, sampleGridFor, opacityGroupPaint, pseudoTextColors, BOLD_WEIGHT, hasPaintEffects, isolatedBlendBackdrop, labelReferrers, beneathOpaqueAncestor } from '../../lib/contrast.js';
|
|
9
|
+
showRatio, asRgb, splitBackgroundLayers, backgroundLayerUrl, sampleGridFor, opacityGroupPaint, pseudoTextColors, BOLD_WEIGHT, hasPaintEffects, isolatedBlendBackdrop, labelReferrers, beneathOpaqueAncestor, lazyWithheldBackground } from '../../lib/contrast.js';
|
|
10
10
|
|
|
11
11
|
/** The first url() among a background-image list's layers, or null. */
|
|
12
12
|
const firstLayerUrl = (css) => splitBackgroundLayers(css ?? '').map(backgroundLayerUrl).find(Boolean) ?? null;
|
|
@@ -1508,6 +1508,16 @@ export function createContrastRule({ id, name, tags, help, helpUrl, thresholds }
|
|
|
1508
1508
|
rule.evaluate = async (element, helpers) => {
|
|
1509
1509
|
const verdict = await judge(element, helpers);
|
|
1510
1510
|
if (verdict.status !== 'fail') return verdict;
|
|
1511
|
+
// The background this was measured against is a placeholder: the page
|
|
1512
|
+
// holds its background image back until a lazy loader marks it loaded,
|
|
1513
|
+
// which happens before the visitor scrolls it into view. A failure has
|
|
1514
|
+
// to be shown against the paint the visitor sees.
|
|
1515
|
+
if (lazyWithheldBackground(element)) {
|
|
1516
|
+
return {
|
|
1517
|
+
status: 'incomplete',
|
|
1518
|
+
message: 'This text sits in a section whose background image is lazy-loaded and has not loaded yet, so it was measured against a background visitors never see. Scroll the page to this text and run the audit again, or check it by eye.',
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1511
1521
|
// A miss on a glyph that is not words: see glyphNotWords. Every fail
|
|
1512
1522
|
// path passes through here, so this is said once. A glyph still owes
|
|
1513
1523
|
// 3:1 under 1.4.11 when it is needed to identify a control, so one that
|
package/package.json
CHANGED