mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 00:56:30 +03:00
Detect active container query CSS
Use a temporary custom-property probe so the browser decides whether a nested style rule actually applies in the current container layout. AI assistance disclosure: Codex helped implement and test this fix under maintainer direction.
This commit is contained in:
@@ -1306,6 +1306,45 @@ if (IS_BROWSER) {
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
let containerProbeSequence = 0;
|
||||
|
||||
function isContainerCssRule(rule) {
|
||||
return rule?.constructor?.name === 'CSSContainerRule'
|
||||
|| /^\s*@container\b/i.test(rule?.cssText || '');
|
||||
}
|
||||
|
||||
function styleRuleAppliesToLiveMatches(rule, matches) {
|
||||
const style = rule?.style;
|
||||
if (!style || !matches?.length || typeof getComputedStyle !== 'function') return false;
|
||||
const sequence = ++containerProbeSequence;
|
||||
const property = `--impeccable-container-probe-${sequence}-${Math.random().toString(36).slice(2)}`;
|
||||
const value = `impeccable-container-active-${sequence}`;
|
||||
const previousValue = style.getPropertyValue(property);
|
||||
const previousPriority = style.getPropertyPriority(property);
|
||||
try {
|
||||
style.setProperty(property, value, 'important');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pseudoElements = [...new Set(
|
||||
String(rule.selectorText || '').match(/::[a-zA-Z-]+(?:\([^)]*\))?/g) || [],
|
||||
)];
|
||||
try {
|
||||
return matches.some(el => [null, ...pseudoElements].some(pseudo => {
|
||||
try {
|
||||
const computed = pseudo ? getComputedStyle(el, pseudo) : getComputedStyle(el);
|
||||
return computed.getPropertyValue(property).trim() === value;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
} finally {
|
||||
if (previousValue) style.setProperty(property, previousValue, previousPriority);
|
||||
else style.removeProperty(property);
|
||||
}
|
||||
}
|
||||
|
||||
function conditionalCssRuleIsActive(rule) {
|
||||
const type = Number(rule?.type);
|
||||
const constructorName = rule?.constructor?.name || '';
|
||||
@@ -1321,13 +1360,6 @@ if (IS_BROWSER) {
|
||||
try { return CSS.supports(condition); }
|
||||
catch { return true; }
|
||||
}
|
||||
if (constructorName === 'CSSContainerRule' || /^\s*@container\b/i.test(rule?.cssText || '')) {
|
||||
// The platform exposes no matchMedia-equivalent for container queries,
|
||||
// and selector matches do not prove that the element's query container
|
||||
// satisfies the condition. Omit the group rather than report styles that
|
||||
// are inactive for the current layout.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1341,7 +1373,7 @@ if (IS_BROWSER) {
|
||||
function linkedStylesheetText() {
|
||||
const parts = [];
|
||||
const seen = new Set();
|
||||
const appendRules = (rules) => {
|
||||
const appendRules = (rules, requiresAppliedMatch = false) => {
|
||||
for (const rule of rules) {
|
||||
if (rule.styleSheet) {
|
||||
appendSheet(rule.styleSheet);
|
||||
@@ -1353,7 +1385,12 @@ if (IS_BROWSER) {
|
||||
// Only declarations with a resolvable live host enter the corpus.
|
||||
// Unresolvable selectors are uncertain, not evidence that a pattern
|
||||
// rendered, and retaining them would leak unused CSS into findings.
|
||||
if (matches?.length > 0) parts.push(cssText);
|
||||
if (
|
||||
matches?.length > 0
|
||||
&& (!requiresAppliedMatch || styleRuleAppliesToLiveMatches(rule, matches))
|
||||
) {
|
||||
parts.push(cssText);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let nested = [];
|
||||
@@ -1368,7 +1405,7 @@ if (IS_BROWSER) {
|
||||
const isKeyframes = /^\s*@(?:-webkit-)?keyframes\b/i.test(cssText);
|
||||
if (hasNestedRules && !isKeyframes) {
|
||||
if (!conditionalCssRuleIsActive(rule)) continue;
|
||||
appendRules(nested);
|
||||
appendRules(nested, requiresAppliedMatch || isContainerCssRule(rule));
|
||||
continue;
|
||||
}
|
||||
if (cssText) parts.push(cssText);
|
||||
|
||||
@@ -8210,6 +8210,45 @@ if (IS_BROWSER) {
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
let containerProbeSequence = 0;
|
||||
|
||||
function isContainerCssRule(rule) {
|
||||
return rule?.constructor?.name === 'CSSContainerRule'
|
||||
|| /^\s*@container\b/i.test(rule?.cssText || '');
|
||||
}
|
||||
|
||||
function styleRuleAppliesToLiveMatches(rule, matches) {
|
||||
const style = rule?.style;
|
||||
if (!style || !matches?.length || typeof getComputedStyle !== 'function') return false;
|
||||
const sequence = ++containerProbeSequence;
|
||||
const property = `--impeccable-container-probe-${sequence}-${Math.random().toString(36).slice(2)}`;
|
||||
const value = `impeccable-container-active-${sequence}`;
|
||||
const previousValue = style.getPropertyValue(property);
|
||||
const previousPriority = style.getPropertyPriority(property);
|
||||
try {
|
||||
style.setProperty(property, value, 'important');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pseudoElements = [...new Set(
|
||||
String(rule.selectorText || '').match(/::[a-zA-Z-]+(?:\([^)]*\))?/g) || [],
|
||||
)];
|
||||
try {
|
||||
return matches.some(el => [null, ...pseudoElements].some(pseudo => {
|
||||
try {
|
||||
const computed = pseudo ? getComputedStyle(el, pseudo) : getComputedStyle(el);
|
||||
return computed.getPropertyValue(property).trim() === value;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
} finally {
|
||||
if (previousValue) style.setProperty(property, previousValue, previousPriority);
|
||||
else style.removeProperty(property);
|
||||
}
|
||||
}
|
||||
|
||||
function conditionalCssRuleIsActive(rule) {
|
||||
const type = Number(rule?.type);
|
||||
const constructorName = rule?.constructor?.name || '';
|
||||
@@ -8225,13 +8264,6 @@ if (IS_BROWSER) {
|
||||
try { return CSS.supports(condition); }
|
||||
catch { return true; }
|
||||
}
|
||||
if (constructorName === 'CSSContainerRule' || /^\s*@container\b/i.test(rule?.cssText || '')) {
|
||||
// The platform exposes no matchMedia-equivalent for container queries,
|
||||
// and selector matches do not prove that the element's query container
|
||||
// satisfies the condition. Omit the group rather than report styles that
|
||||
// are inactive for the current layout.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8245,7 +8277,7 @@ if (IS_BROWSER) {
|
||||
function linkedStylesheetText() {
|
||||
const parts = [];
|
||||
const seen = new Set();
|
||||
const appendRules = (rules) => {
|
||||
const appendRules = (rules, requiresAppliedMatch = false) => {
|
||||
for (const rule of rules) {
|
||||
if (rule.styleSheet) {
|
||||
appendSheet(rule.styleSheet);
|
||||
@@ -8257,7 +8289,12 @@ if (IS_BROWSER) {
|
||||
// Only declarations with a resolvable live host enter the corpus.
|
||||
// Unresolvable selectors are uncertain, not evidence that a pattern
|
||||
// rendered, and retaining them would leak unused CSS into findings.
|
||||
if (matches?.length > 0) parts.push(cssText);
|
||||
if (
|
||||
matches?.length > 0
|
||||
&& (!requiresAppliedMatch || styleRuleAppliesToLiveMatches(rule, matches))
|
||||
) {
|
||||
parts.push(cssText);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let nested = [];
|
||||
@@ -8272,7 +8309,7 @@ if (IS_BROWSER) {
|
||||
const isKeyframes = /^\s*@(?:-webkit-)?keyframes\b/i.test(cssText);
|
||||
if (hasNestedRules && !isKeyframes) {
|
||||
if (!conditionalCssRuleIsActive(rule)) continue;
|
||||
appendRules(nested);
|
||||
appendRules(nested, requiresAppliedMatch || isContainerCssRule(rule));
|
||||
continue;
|
||||
}
|
||||
if (cssText) parts.push(cssText);
|
||||
|
||||
@@ -1405,11 +1405,12 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
})));
|
||||
const linkedFindings = await linkedPage.evaluate(() => window.impeccableDetect({ serialize: true })
|
||||
.flatMap(group => group.findings || []));
|
||||
const inactiveContainerBackground = await linkedPage.$eval(
|
||||
'.inactive-container-stripes',
|
||||
element => getComputedStyle(element).backgroundImage,
|
||||
);
|
||||
assert.equal(inactiveContainerBackground, 'none');
|
||||
const containerBackgrounds = await linkedPage.evaluate(() => ({
|
||||
inactive: getComputedStyle(document.querySelector('.inactive-container-stripes')).backgroundImage,
|
||||
active: getComputedStyle(document.querySelector('.active-container-halo')).backgroundImage,
|
||||
}));
|
||||
assert.equal(containerBackgrounds.inactive, 'none');
|
||||
assert.match(containerBackgrounds.active, /radial-gradient/i);
|
||||
const grids = linkedFindings.filter(finding => finding.type === 'codex-grid-background');
|
||||
assert.equal(grids.length, 1, JSON.stringify({ linkedFindings, linkedCssom }));
|
||||
assert.equal(grids[0].severity, 'advisory');
|
||||
@@ -1419,6 +1420,7 @@ describe('detectUrl — browser-only fixtures', () => {
|
||||
true,
|
||||
JSON.stringify({ linkedFindings, linkedCssom }),
|
||||
);
|
||||
assert.equal(linkedFindings.some(finding => finding.type === 'radial-halo'), true);
|
||||
assert.equal(linkedFindings.some(finding => finding.type === 'repeating-stripes-gradient'), false);
|
||||
assert.equal(linkedFindings.some(finding => finding.type === 'layout-transition'), false);
|
||||
await linkedPage.close();
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: #111;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* A valid hostless pseudo-element selector cannot be queried through the DOM
|
||||
selector API. It must remain in the corpus rather than count as unused. */
|
||||
main > ::before {
|
||||
@@ -45,10 +50,20 @@ main > ::before {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.container-query-host-active {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
@container (width > 900px) {
|
||||
.inactive-container-stripes {
|
||||
background: repeating-linear-gradient(45deg, #eee, #eee 10px, #fafafa 10px, #fafafa 20px);
|
||||
}
|
||||
|
||||
.active-container-halo {
|
||||
width: 640px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, rgba(80, 111, 255, 0.85), transparent 70%);
|
||||
}
|
||||
}
|
||||
|
||||
.unused-linked-transition {
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
<div class="container-query-host">
|
||||
<div class="inactive-container-stripes">Inactive container-query stripes</div>
|
||||
</div>
|
||||
<div class="container-query-host container-query-host-active">
|
||||
<div class="active-container-halo">Active container-query halo</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user