LibWeb: Fix @keyframes resolution for slotted elements

A @keyframes rule scoped to a shadow root was not reliably reached
from an animated slotted light-DOM element: the keyframes lookup
walked the element's own root first, then fell back to the document,
but slotted elements can pick up animation-name from a ::slotted(...)
rule that lives in an ancestor shadow root rather than in the
element's own tree.

Track the shadow-root scope that supplied each winning cascaded
declaration, and use that scope to resolve the matching @keyframes
when processing animation definitions. A shared constructable
stylesheet can be adopted into several scopes at once, so the
declaration object alone is too weak as a key; the per-entry
shadow-root pointer disambiguates which adoption actually contributed.

Also refresh running CSS animations' keyframe sets when style is
recomputed. Previously only the first animation creation path set a
keyframe set, so an existing animation never picked up newly inserted
@keyframes rules.
This commit is contained in:
Andreas Kling
2026-04-22 08:52:18 +02:00
committed by Andreas Kling
parent 654e1efacc
commit 11c75a2ffb
Notes: github-actions[bot] 2026-04-22 19:00:09 +00:00
12 changed files with 194 additions and 23 deletions

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
@keyframes fade {
from { opacity: 0.5; }
to { opacity: 0.5; }
}
</style>
<div id="host">host</div>
<script>
test(() => {
const host = document.getElementById("host");
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
:host {
animation-name: fade;
animation-duration: 1s;
animation-fill-mode: both;
}
@keyframes fade {
from { opacity: 0.25; }
to { opacity: 0.25; }
}
</style>
<slot></slot>
`;
println(`opacity: ${getComputedStyle(host).opacity}`);
});
</script>

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
#slotted {
animation-name: fade;
animation-duration: 1s;
animation-fill-mode: both;
}
@keyframes fade {
from { opacity: 0.5; }
to { opacity: 0.5; }
}
</style>
<div id="host"><span id="slotted">slotted</span></div>
<script>
test(() => {
const host = document.getElementById("host");
const slotted = document.getElementById("slotted");
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
@keyframes fade {
from { opacity: 0.25; }
to { opacity: 0.25; }
}
</style>
<slot></slot>
`;
println(`opacity: ${getComputedStyle(slotted).opacity}`);
});
</script>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
@keyframes fade {
from { opacity: 0.5; }
to { opacity: 0.5; }
}
</style>
<div id="host"><span id="slotted">slotted</span></div>
<script>
test(() => {
const host = document.getElementById("host");
const slotted = document.getElementById("slotted");
const shadowRoot = host.attachShadow({ mode: "open" });
const sharedSheet = new CSSStyleSheet();
sharedSheet.replaceSync(`
#slotted {
animation-name: fade;
animation-duration: 1s;
animation-fill-mode: both;
}
`);
const shadowKeyframesSheet = new CSSStyleSheet();
shadowKeyframesSheet.replaceSync(`
@keyframes fade {
from { opacity: 0.25; }
to { opacity: 0.25; }
}
`);
document.adoptedStyleSheets = [sharedSheet];
shadowRoot.innerHTML = "<slot></slot>";
shadowRoot.adoptedStyleSheets = [sharedSheet, shadowKeyframesSheet];
println(`opacity: ${getComputedStyle(slotted).opacity}`);
});
</script>

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
@keyframes fade {
from { opacity: 0.5; }
to { opacity: 0.5; }
}
</style>
<div id="host"><span id="slotted" class="item">slotted</span></div>
<script>
test(() => {
const slotted = document.getElementById("slotted");
const shadowRoot = document.getElementById("host").attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>
::slotted(.item) {
animation-name: fade;
animation-duration: 1s;
animation-fill-mode: both;
}
@keyframes fade {
from { opacity: 0.25; }
to { opacity: 0.25; }
}
</style>
<slot></slot>
`;
println(`opacity: ${getComputedStyle(slotted).opacity}`);
});
</script>