Fix :has() invalidation error with nesting parent containing hover
Sets ContainsPseudoInsideHasPseudoClass flag of a :has() selector if its
argument contains nesting parent.
Currently, the CSSSelectorParser doesn't set the above flag if the :has()
selector's argument has a nesting parent selector and the nesting parent
contains a pseudo selector.
Due to this, even if the pseudo state in the nesting parent is changed,
StyleEngine skips :has() invalidation for the pseudo state change because
the :has() anchor element doesn't have AffectedByPseudoInHas flag set.
To fix this, CSSSelectorParser sets ContainsPseudoInsideHasPseudoClass
flag if the :has() selector contains a nesting parent selector in its
argument selector.
Bug: 1517866
Change-Id: I41b6f69a83db41a4e519490018bfb214a724f807
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5203146
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Byungwoo Lee <blee@igalia.com>
Cr-Commit-Position: refs/heads/main@{#1248019}
diff --git a/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc b/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc
index 38d5324..db6547d 100644
--- a/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc
+++ b/third_party/blink/renderer/core/css/affected_by_pseudo_test.cc
@@ -5391,4 +5391,63 @@
ASSERT_EQ(GetStyleEngine().StyleForElementCount() - start_count, 1U);
}
+TEST_F(AffectedByPseudoTest, AffectedByPseudoInHasWithNestingParent) {
+ SetHtmlInnerHTML(R"HTML(
+ <style>
+ .b:hover {
+ .a:has(~ &) { background-color: green; }
+ }
+ </style>
+ <div id=div1></div>
+ <div id=div2 class='a'></div>
+ <div id=div3></div>
+ <div id=div4 class='b'></div>
+ <div id=div5></div>
+ )HTML");
+
+ UpdateAllLifecyclePhasesForTest();
+ CheckAffectedByFlagsForHas(
+ "div1", {{kAffectedBySubjectHas, false},
+ {kAffectedByPseudoInHas, false},
+ {kSiblingsAffectedByHasForSiblingRelationship, false},
+ {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+ CheckAffectedByFlagsForHas(
+ "div2", {{kAffectedBySubjectHas, true},
+ {kAffectedByPseudoInHas, true},
+ {kSiblingsAffectedByHasForSiblingRelationship, true},
+ {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+ CheckAffectedByFlagsForHas(
+ "div3", {{kAffectedBySubjectHas, false},
+ {kAffectedByPseudoInHas, false},
+ {kSiblingsAffectedByHasForSiblingRelationship, true},
+ {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+ CheckAffectedByFlagsForHas(
+ "div4", {{kAffectedBySubjectHas, false},
+ {kAffectedByPseudoInHas, false},
+ {kSiblingsAffectedByHasForSiblingRelationship, true},
+ {kAncestorsOrSiblingsAffectedByHoverInHas, true}});
+ CheckAffectedByFlagsForHas(
+ "div5", {{kAffectedBySubjectHas, false},
+ {kAffectedByPseudoInHas, false},
+ {kSiblingsAffectedByHasForSiblingRelationship, true},
+ {kAncestorsOrSiblingsAffectedByHoverInHas, false}});
+
+ unsigned start_count = GetStyleEngine().StyleForElementCount();
+ GetElementById("div3")->SetHovered(true);
+ UpdateAllLifecyclePhasesForTest();
+ unsigned element_count =
+ GetStyleEngine().StyleForElementCount() - start_count;
+ ASSERT_EQ(0U, element_count);
+ GetElementById("div3")->SetHovered(false);
+ UpdateAllLifecyclePhasesForTest();
+
+ start_count = GetStyleEngine().StyleForElementCount();
+ GetElementById("div4")->SetHovered(true);
+ UpdateAllLifecyclePhasesForTest();
+ element_count = GetStyleEngine().StyleForElementCount() - start_count;
+ ASSERT_EQ(2U, element_count);
+ GetElementById("div4")->SetHovered(false);
+ UpdateAllLifecyclePhasesForTest();
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/core/css/parser/css_selector_parser.cc b/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
index 70e370a..908f33b 100644
--- a/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
+++ b/third_party/blink/renderer/core/css/parser/css_selector_parser.cc
@@ -1694,6 +1694,18 @@
output_.push_back(
CSSSelector(parent_rule_for_nesting_, /*is_implicit=*/false));
+
+ if (is_inside_has_argument_) {
+ // In case that a nesting parent selector is inside a :has() pseudo class,
+ // mark the :has() containing a pseudo selector so that the StyleEngine can
+ // invalidate the anchor element of the :has() for a pseudo state change
+ // in the parent selector. (crbug.com/1517866)
+ // This ignores whether the nesting parent actually contains a pseudo to
+ // avoid nesting parent lookup overhead and the complexity caused by
+ // reparenting style rules.
+ found_pseudo_in_has_argument_ = true;
+ }
+
return true;
}
diff --git a/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-hover.html b/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-hover.html
new file mode 100644
index 0000000..8082980e
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/selectors/invalidation/has-with-nesting-parent-containing-hover.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Selector Invalidation: :has() with nesting parent containing :hover</title>
+<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<style>
+ dd, dt { background: white; }
+ dd:hover {
+ dt:has(~ &) { background: lime; }
+ }
+</style>
+<dt id=dt1>#dt1</dt>
+<dd id=dd1>#dd1, Hover me, the DT above should go lime</dd>
+<dt id=dt2>#dt2</dt>
+<dd id=dd2>#dd2, Hover me, both DTs above should go lime</dd>
+<script>
+ const white = 'rgb(255, 255, 255)';
+ const lime = 'rgb(0, 255, 0)';
+
+ function bg_color(element, color, message) {
+ assert_equals(getComputedStyle(element)['background-color'], color, message);
+ }
+
+ promise_test(async () => {
+ bg_color(dt1, white, "#dt1 initially white");
+ bg_color(dd1, white, "#dd1 initially white");
+ bg_color(dt2, white, "#dt2 initially white");
+ bg_color(dd2, white, "#dd2 initially white");
+
+ await new test_driver.Actions().pointerMove(0, 0, {origin: dd1}).send();
+
+ bg_color(dt1, lime, "#dt1 should go lime when hover #dd1");
+ bg_color(dd1, white, "#dd1 doesn't change when hover #dd1");
+ bg_color(dt2, white, "#dt2 doesn't change when hover #dd1");
+ bg_color(dd2, white, "#dd2 doesn't change when hover #dd1");
+
+ await new test_driver.Actions().pointerMove(0, 0, {origin: dt1}).send();
+
+ bg_color(dt1, white, "#dt1 should go white when hover #dt2");
+ bg_color(dd1, white, "#dd1 doesn't change when hover #dt2");
+ bg_color(dt2, white, "#dt2 doesn't change when hover #dt2");
+ bg_color(dd2, white, "#dd2 doesn't change when hover #dt2");
+
+ await new test_driver.Actions().pointerMove(0, 0, {origin: dd2}).send();
+
+ bg_color(dt1, lime, "#dt1 should go lime when hover #dd2");
+ bg_color(dd1, white, "#dd1 doesn't change when hover #dd2");
+ bg_color(dt2, lime, "#dt2 should go lime when hover #dd2");
+ bg_color(dd2, white, "#dd2 doesn't change when hover #dd2");
+ });
+</script>
\ No newline at end of file