LibWeb: Invalidate sibling style for :only-child and :*-of-type

After f7a3f785a8, sibling nodes' styles
were no longer invalidated after a node was removed. This reuses the
flag for `:first-child` and `:last-child` to indicate that a node's
style might be affected by any structural change in its siblings.

Fixes #4631.

Resolves the `:only-child` ACID3 failure as documented in #1231.
This commit is contained in:
Jelle Raaijmakers 2025-05-07 12:27:42 +02:00 committed by Alexander Kalenik
commit c56f7d9cde
Notes: github-actions[bot] 2025-05-07 11:56:29 +00:00
5 changed files with 52 additions and 7 deletions

View file

@ -480,15 +480,18 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
} }
case CSS::PseudoClass::FirstChild: case CSS::PseudoClass::FirstChild:
if (context.collect_per_element_selector_involvement_metadata) { if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_first_or_last_child_pseudo_class(true); const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
} }
return !element.previous_element_sibling(); return !element.previous_element_sibling();
case CSS::PseudoClass::LastChild: case CSS::PseudoClass::LastChild:
if (context.collect_per_element_selector_involvement_metadata) { if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_first_or_last_child_pseudo_class(true); const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
} }
return !element.next_element_sibling(); return !element.next_element_sibling();
case CSS::PseudoClass::OnlyChild: case CSS::PseudoClass::OnlyChild:
if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
}
return !(element.previous_element_sibling() || element.next_element_sibling()); return !(element.previous_element_sibling() || element.next_element_sibling());
case CSS::PseudoClass::Empty: { case CSS::PseudoClass::Empty: {
if (!element.has_children()) if (!element.has_children())
@ -514,10 +517,19 @@ static inline bool matches_pseudo_class(CSS::Selector::SimpleSelector::PseudoCla
case CSS::PseudoClass::Scope: case CSS::PseudoClass::Scope:
return scope ? &element == scope : is<HTML::HTMLHtmlElement>(element); return scope ? &element == scope : is<HTML::HTMLHtmlElement>(element);
case CSS::PseudoClass::FirstOfType: case CSS::PseudoClass::FirstOfType:
if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
}
return !previous_sibling_with_same_tag_name(element); return !previous_sibling_with_same_tag_name(element);
case CSS::PseudoClass::LastOfType: case CSS::PseudoClass::LastOfType:
if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
}
return !next_sibling_with_same_tag_name(element); return !next_sibling_with_same_tag_name(element);
case CSS::PseudoClass::OnlyOfType: case CSS::PseudoClass::OnlyOfType:
if (context.collect_per_element_selector_involvement_metadata) {
const_cast<DOM::Element&>(element).set_affected_by_sibling_position_or_count_pseudo_class(true);
}
return !previous_sibling_with_same_tag_name(element) && !next_sibling_with_same_tag_name(element); return !previous_sibling_with_same_tag_name(element) && !next_sibling_with_same_tag_name(element);
case CSS::PseudoClass::Lang: case CSS::PseudoClass::Lang:
return matches_lang_pseudo_class(element, pseudo_class.languages); return matches_lang_pseudo_class(element, pseudo_class.languages);

View file

@ -620,7 +620,7 @@ CSS::RequiredInvalidationAfterStyleChange Element::recompute_style()
m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator = false; m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator = false;
m_affected_by_direct_sibling_combinator = false; m_affected_by_direct_sibling_combinator = false;
m_affected_by_indirect_sibling_combinator = false; m_affected_by_indirect_sibling_combinator = false;
m_affected_by_first_or_last_child_pseudo_class = false; m_affected_by_sibling_position_or_count_pseudo_class = false;
m_affected_by_nth_child_pseudo_class = false; m_affected_by_nth_child_pseudo_class = false;
m_sibling_invalidation_distance = 0; m_sibling_invalidation_distance = 0;

View file

@ -453,8 +453,8 @@ public:
bool affected_by_indirect_sibling_combinator() const { return m_affected_by_indirect_sibling_combinator; } bool affected_by_indirect_sibling_combinator() const { return m_affected_by_indirect_sibling_combinator; }
void set_affected_by_indirect_sibling_combinator(bool value) { m_affected_by_indirect_sibling_combinator = value; } void set_affected_by_indirect_sibling_combinator(bool value) { m_affected_by_indirect_sibling_combinator = value; }
bool affected_by_first_or_last_child_pseudo_class() const { return m_affected_by_first_or_last_child_pseudo_class; } bool affected_by_sibling_position_or_count_pseudo_class() const { return m_affected_by_sibling_position_or_count_pseudo_class; }
void set_affected_by_first_or_last_child_pseudo_class(bool value) { m_affected_by_first_or_last_child_pseudo_class = value; } void set_affected_by_sibling_position_or_count_pseudo_class(bool value) { m_affected_by_sibling_position_or_count_pseudo_class = value; }
bool affected_by_nth_child_pseudo_class() const { return m_affected_by_nth_child_pseudo_class; } bool affected_by_nth_child_pseudo_class() const { return m_affected_by_nth_child_pseudo_class; }
void set_affected_by_nth_child_pseudo_class(bool value) { m_affected_by_nth_child_pseudo_class = value; } void set_affected_by_nth_child_pseudo_class(bool value) { m_affected_by_nth_child_pseudo_class = value; }
@ -464,7 +464,7 @@ public:
bool style_affected_by_structural_changes() const bool style_affected_by_structural_changes() const
{ {
return affected_by_direct_sibling_combinator() || affected_by_indirect_sibling_combinator() || affected_by_first_or_last_child_pseudo_class() || affected_by_nth_child_pseudo_class(); return affected_by_direct_sibling_combinator() || affected_by_indirect_sibling_combinator() || affected_by_sibling_position_or_count_pseudo_class() || affected_by_nth_child_pseudo_class();
} }
size_t number_of_owned_list_items() const; size_t number_of_owned_list_items() const;
@ -573,7 +573,7 @@ private:
bool m_affected_by_has_pseudo_class_in_non_subject_position : 1 { false }; bool m_affected_by_has_pseudo_class_in_non_subject_position : 1 { false };
bool m_affected_by_direct_sibling_combinator : 1 { false }; bool m_affected_by_direct_sibling_combinator : 1 { false };
bool m_affected_by_indirect_sibling_combinator : 1 { false }; bool m_affected_by_indirect_sibling_combinator : 1 { false };
bool m_affected_by_first_or_last_child_pseudo_class : 1 { false }; bool m_affected_by_sibling_position_or_count_pseudo_class : 1 { false };
bool m_affected_by_nth_child_pseudo_class : 1 { false }; bool m_affected_by_nth_child_pseudo_class : 1 { false };
bool m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator : 1 { false }; bool m_affected_by_has_pseudo_class_with_relative_selector_that_has_sibling_combinator : 1 { false };

View file

@ -0,0 +1,3 @@
div#a p: rgb(0, 128, 0)
div#b p: rgb(0, 128, 0)
div#c p: rgb(0, 128, 0)

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<style>
div#a p:only-child {
background-color: green;
}
div#b p:first-of-type {
background-color: green;
}
div#c p:last-of-type {
background-color: green;
}
</style>
<div id="a"><p>foo</p><p>bar</p></div>
<div id="b"><p>foo</p><p>bar</p></div>
<div id="c"><p>foo</p><p>bar</p></div>
<script>
test(() => {
document.body.offsetWidth; // force layout
document.querySelector('div#a p:last-child').remove();
println(`div#a p: ${getComputedStyle(document.querySelector('div#a p')).backgroundColor}`);
document.querySelector('div#b p:first-child').remove();
println(`div#b p: ${getComputedStyle(document.querySelector('div#b p')).backgroundColor}`);
document.querySelector('div#c p:last-child').remove();
println(`div#c p: ${getComputedStyle(document.querySelector('div#c p')).backgroundColor}`);
});
</script>