LibWeb: Refresh stale inherited custom-property data on ancestor change

When an ancestor's custom property changes, intermediate elements that
don't themselves consume `var()`/`inherit()` were skipped during style
recomputation, leaving their `m_custom_property_data` pointing at the
ancestor's stale `CustomPropertyData`. Deeper descendants that did
consume `var()` then resolved against the out-of-date chain.

This change adds `Element::refresh_inherited_custom_property_data()` to
re-link an element's inherited chain to its parent's current data
without re-cascading. This is called during `update_style_recursively()`
for elements that don't need a full recompute.
This commit is contained in:
Tim Ledbetter 2026-05-12 23:07:47 +01:00 committed by Andreas Kling
parent 82b016ed19
commit da210eaa9d
7 changed files with 83 additions and 7 deletions

View file

@ -1845,8 +1845,11 @@ bool Document::layout_is_up_to_date() const
|| (recompute_elements_depending_on_custom_properties && (element.style_uses_var_css_function() || element.style_uses_inherit_css_function()))
|| needs_style_update_due_to_if_media) {
node_invalidation = element.recompute_style(did_change_custom_properties);
} else if (needs_inherited_style_update) {
node_invalidation = element.recompute_inherited_style();
} else {
if (needs_inherited_style_update)
node_invalidation = element.recompute_inherited_style();
if (recompute_elements_depending_on_custom_properties && element.refresh_inherited_custom_property_data())
did_change_custom_properties = true;
}
is_display_none = static_cast<Element&>(node).computed_properties()->display().is_none();

View file

@ -28,6 +28,7 @@
#include <LibWeb/CSS/CSSStyleProperties.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/CountersSet.h>
#include <LibWeb/CSS/CustomPropertyData.h>
#include <LibWeb/CSS/Invalidation/AttributeInvalidator.h>
#include <LibWeb/CSS/Invalidation/CustomElementInvalidator.h>
#include <LibWeb/CSS/Invalidation/ElementStateInvalidator.h>
@ -3561,6 +3562,20 @@ RefPtr<CSS::CustomPropertyData const> Element::custom_property_data(Optional<CSS
return ensure_pseudo_element(pseudo_element.value()).custom_property_data();
}
bool Element::refresh_inherited_custom_property_data()
{
RefPtr<CSS::CustomPropertyData const> parent_data;
if (auto inherit_from = element_to_inherit_style_from({})) {
if (auto data = inherit_from->custom_property_data({}))
parent_data = data->inheritable(document());
}
if (m_custom_property_data == parent_data)
return false;
m_custom_property_data = move(parent_data);
return true;
}
// https://drafts.csswg.org/cssom-view/#dom-element-scroll
GC::Ref<WebIDL::Promise> Element::scroll(double x, double y)
{

View file

@ -268,6 +268,8 @@ public:
void set_custom_property_data(Optional<CSS::PseudoElement>, RefPtr<CSS::CustomPropertyData const>);
[[nodiscard]] RefPtr<CSS::CustomPropertyData const> custom_property_data(Optional<CSS::PseudoElement>) const;
[[nodiscard]] bool refresh_inherited_custom_property_data();
bool style_uses_attr_css_function() const { return m_style_uses_attr_css_function; }
void set_style_uses_attr_css_function() { m_style_uses_attr_css_function = true; }
bool style_uses_var_css_function() const { return m_style_uses_var_css_function; }

View file

@ -0,0 +1,5 @@
case 1 initial display: inline
case 1 final display: inline-flex
case 1 final size: 24x24
case 2 width: 20px
case 2 color: rgb(0, 128, 0)

View file

@ -32,8 +32,8 @@ pseudo::before --pseudo-var: from-pseudo
pseudo::before --inherited-var: from-element
=== Dynamic update ===
before: (empty)
after:
changed:
after: hello
changed: world
removed: (empty)
=== Nonexistent ===
nonexistent: (empty)

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 3 tests
2 Pass
1 Fail
3 Pass
Pass inherit() invalidates when values changes on parent element, inherited
Fail inherit() invalidates when values changes on parent element, inherited, deeper
Pass inherit() invalidates when values changes on parent element, inherited, deeper
Pass inherit() invalidates when values changes on parent element, non-inherited

View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
test(() => {
class MyIcon extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.textContent = `
:host {
display: var(--my-display);
width: 24px;
height: 24px;
}
`;
shadow.appendChild(style);
}
}
customElements.define("my-icon", MyIcon);
const icon = document.createElement("my-icon");
document.body.appendChild(icon);
const initialDisplay = getComputedStyle(icon).display;
const iconStyleElement = document.createElement("style");
iconStyleElement.textContent = `html { --my-display: inline-flex; }`;
document.head.appendChild(iconStyleElement);
const iconRect = icon.getBoundingClientRect();
println(`case 1 initial display: ${initialDisplay}`);
println(`case 1 final display: ${getComputedStyle(icon).display}`);
println(`case 1 final size: ${iconRect.width}x${iconRect.height}`);
icon.remove();
iconStyleElement.remove();
const combo = document.createElement("div");
combo.style.width = "var(--size)";
document.body.appendChild(combo);
getComputedStyle(combo).width;
const comboStyleElement = document.createElement("style");
comboStyleElement.textContent = `html { --size: 20px; color: green; }`;
document.head.appendChild(comboStyleElement);
println(`case 2 width: ${getComputedStyle(combo).width}`);
println(`case 2 color: ${getComputedStyle(combo).color}`);
combo.remove();
comboStyleElement.remove();
});
</script>