LibWeb: Delay generic :has() sibling scans until sibling roots

Mark elements reached by stepping through sibling combinators inside
:has() and use that breadcrumb during generic invalidation walks.

Keep the existing conservative sibling scans for mutations outside
those marked subtrees so nested :is(), :not(), and nesting cases
continue to invalidate correctly.

Also keep :has() eager within compounds that contain ::part(). Those
selectors retarget the remaining simple selectors to the part host, so
deferring :has() there changes which element the pseudo-class runs
against and can make ::part(foo):has(.match) spuriously match.

Add a counter-based sibling-scan test and a regression test covering
the ::part()/ :has() selector orderings.
This commit is contained in:
Andreas Kling
2026-04-18 23:38:57 +02:00
committed by Alexander Kalenik
parent 85ff13870f
commit 7a5b1d9de1
Notes: github-actions[bot] 2026-04-20 11:21:48 +00:00
7 changed files with 118 additions and 12 deletions

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
.anchor:has(+ .wrapper .match) { color: red; }
</style>
<div id="outer">
<div id="anchor" class="anchor">anchor</div>
<div class="wrapper">
<div>
<div>
<span id="target">target</span>
</div>
</div>
</div>
<div>unrelated sibling</div>
</div>
<script>
function printCounters() {
let counters = internals.getStyleInvalidationCounters();
println(`hasAncestorWalkInvocations: ${counters.hasAncestorWalkInvocations}`);
println(`hasAncestorWalkVisits: ${counters.hasAncestorWalkVisits}`);
println(`hasAncestorSiblingElementChecks: ${counters.hasAncestorSiblingElementChecks}`);
println(`hasMatchInvocations: ${counters.hasMatchInvocations}`);
println(`hasResultCacheHits: ${counters.hasResultCacheHits}`);
println(`hasResultCacheMisses: ${counters.hasResultCacheMisses}`);
println(`styleInvalidations: ${counters.styleInvalidations}`);
}
test(() => {
let anchor = document.getElementById("anchor");
let target = document.getElementById("target");
getComputedStyle(anchor).color;
internals.resetStyleInvalidationCounters();
target.classList.add("match");
getComputedStyle(anchor).color;
printCounters();
});
</script>

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<style>
#host::part(p):has(.match) { background-color: red; }
#host:has(.match)::part(p) { color: green; }
</style>
<div id="host">
<span class="match"></span>
</div>
<script src="../include.js"></script>
<script>
test(() => {
const host = document.getElementById("host");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `<span id="inner" part="p">text</span>`;
const inner = shadow.getElementById("inner");
println(getComputedStyle(inner).backgroundColor);
println(getComputedStyle(inner).color);
});
</script>