commit 7b0c7c8034392fdb8cd13f220973f47927c4bfc4
parent 453decad1a322d0f4fed450cd44fb2cd68c4a1b1
Author: David Shin <dshin@mozilla.com>
Date: Wed, 17 Dec 2025 19:11:35 +0000
Bug 2006002: Ensure that positioned elements with implicit anchors get scroll compensated. r=layout-anchor-positioning-reviewers,firefox-style-system-reviewers,layout-reviewers,emilio
Differential Revision: https://phabricator.services.mozilla.com/D276884
Diffstat:
5 files changed, 162 insertions(+), 29 deletions(-)
diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
@@ -11571,17 +11571,26 @@ struct AffectedAnchorGroup {
nsTArray<AffectedAnchor> mFrames;
};
+static const nsIFrame* NearestScrollContainerOfAffectedAnchor(
+ const nsIFrame* aAnchor, const ScrollContainerFrame* aScrollContainer) {
+ const auto* scrollContainer =
+ AnchorPositioningUtils::GetNearestScrollFrame(aAnchor).mScrollContainer;
+ if (!scrollContainer) {
+ // Fixed-pos anchor, likely
+ return nullptr;
+ }
+ // Does this scroll container match a anchor's nearest scroll container,
+ // or contain it?
+ if (scrollContainer == aScrollContainer ||
+ nsLayoutUtils::IsProperAncestorFrame(aScrollContainer, scrollContainer)) {
+ return scrollContainer;
+ }
+ return nullptr;
+}
+
static nsTArray<AffectedAnchorGroup> FindAnchorsAffectedByScroll(
const nsTHashMap<RefPtr<const nsAtom>, nsTArray<nsIFrame*>>& aAnchors,
const ScrollContainerFrame* aScrollContainer) {
- const auto AffectedByScrollContainer =
- [](const nsIFrame* aFrame, const ScrollContainerFrame* aScrollContainer) {
- MOZ_ASSERT(aFrame);
- MOZ_ASSERT(aScrollContainer);
- return aFrame == aScrollContainer ||
- nsLayoutUtils::IsProperAncestorFrame(aScrollContainer, aFrame);
- };
-
nsTArray<AffectedAnchorGroup> affectedAnchors;
// We keep only referenced anchors' name in positioned frames to avoid dealing
// with lifetime issues associated with it. Now we need to re-establish that
@@ -11591,14 +11600,8 @@ static nsTArray<AffectedAnchorGroup> FindAnchorsAffectedByScroll(
Maybe<nsTArray<AffectedAnchor>> affected;
for (const auto& frame : anchorFrames) {
const auto* scrollContainer =
- AnchorPositioningUtils::GetNearestScrollFrame(frame).mScrollContainer;
+ NearestScrollContainerOfAffectedAnchor(frame, aScrollContainer);
if (!scrollContainer) {
- // Fixed-pos anchor, likely
- continue;
- }
- // Does this scroll container match a anchor's nearest scroll container,
- // or contain it?
- if (!AffectedByScrollContainer(scrollContainer, aScrollContainer)) {
continue;
}
if (affected.isNothing()) {
@@ -11616,8 +11619,9 @@ static nsTArray<AffectedAnchorGroup> FindAnchorsAffectedByScroll(
// Given a list of anchors affected by scrolling, find one that the given
// positioned frame need to compensate scroll for.
-static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor(
+static Maybe<AffectedAnchor> FindScrollCompensatedAnchor(
const PresShell* aPresShell,
+ const ScrollContainerFrame* aScrolledScrollContainer,
const nsTArray<AffectedAnchorGroup>& aAffectedAnchors,
const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData,
const nsIFrame** aResolvedDefaultAnchor) {
@@ -11650,6 +11654,22 @@ static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor(
return Nothing{};
}
+ if (defaultAnchorName == nsGkAtoms::AnchorPosImplicitAnchor) {
+ // We're not going to find this in `aAffectedAnchors`, which works off of
+ // `PresShell::mAnchorPosAnchors`, which doesn't store implicit anchors.
+ const auto* anchor =
+ AnchorPositioningUtils::GetAnchorPosImplicitAnchor(aPositioned);
+ if (!anchor) {
+ return Nothing{};
+ }
+ const auto* scrollContainer = NearestScrollContainerOfAffectedAnchor(
+ anchor, aScrolledScrollContainer);
+ if (!scrollContainer) {
+ return Nothing{};
+ }
+ return Some(AffectedAnchor{anchor, scrollContainer});
+ }
+
struct Comparator {
bool Equals(const AffectedAnchor& aEntry, const nsIFrame* aFrame) const {
return aEntry.mAnchor == aFrame;
@@ -11672,7 +11692,7 @@ static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor(
break;
}
const auto& info = anchors.ElementAt(idx);
- return SomeRef(info);
+ return Some(info);
}
return Nothing{};
@@ -11726,7 +11746,7 @@ static bool AnchorIsStickyOrChainedToScrollCompensatedAnchor(
// https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift
void PresShell::UpdateAnchorPosForScroll(
const ScrollContainerFrame* aScrollContainer) {
- if (mAnchorPosAnchors.IsEmpty()) {
+ if (mAnchorPosAnchors.IsEmpty() && mAnchorPosPositioned.IsEmpty()) {
return;
}
@@ -11737,10 +11757,7 @@ void PresShell::UpdateAnchorPosForScroll(
// can.
nsTArray<AffectedAnchorGroup> affectedAnchors =
FindAnchorsAffectedByScroll(mAnchorPosAnchors, aScrollContainer);
-
- if (affectedAnchors.IsEmpty()) {
- return;
- }
+ // Affected anchors may be empty, an implicit anchor may have scrolled.
// Now, update all affected positioned elements' scroll offsets.
for (auto* positioned : mAnchorPosPositioned) {
@@ -11750,8 +11767,9 @@ void PresShell::UpdateAnchorPosForScroll(
continue;
}
const nsIFrame* defaultAnchor = nullptr;
- const auto scrollDependency = FindScrollCompensatedAnchor(
- this, affectedAnchors, positioned, *referenceData, &defaultAnchor);
+ const auto scrollDependency =
+ FindScrollCompensatedAnchor(this, aScrollContainer, affectedAnchors,
+ positioned, *referenceData, &defaultAnchor);
const bool offsetChanged = [&]() {
if (!scrollDependency) {
return false;
diff --git a/layout/base/PresShell.h b/layout/base/PresShell.h
@@ -3261,6 +3261,9 @@ class PresShell final : public nsStubDocumentObserver,
// cannot be determined.
nsTArray<AnchorPosAnchorChange> mLazyAnchorPosAnchorChanges;
+ // Note: Does not store implicit anchors, since many elements can be
+ // potential implicit anchors (e.g. pseudo-elements' implicit anchor
+ // is its originating element).
nsTHashMap<RefPtr<const nsAtom>, nsTArray<nsIFrame*>> mAnchorPosAnchors;
nsTArray<nsIFrame*> mAnchorPosPositioned;
diff --git a/layout/style/ComputedStyle.cpp b/layout/style/ComputedStyle.cpp
@@ -432,11 +432,10 @@ void ComputedStyle::DumpMatchedRules() const {
bool ComputedStyle::HasAnchorPosReference() const {
const auto* pos = StylePosition();
- if (pos->mPositionAnchor.IsIdent()) {
- // Short circuit if there's a default anchor defined, even if
- // it may not end up being referenced.
- // If this early return is removed, we'll need to handle mPositionArea
- // explicitly.
+ if (pos->mPositionAnchor.IsIdent() || pos->mPositionAnchor.IsAuto()) {
+ // Short circuit if there's a default anchor defined (Or an implicit one),
+ // even if it may not end up being referenced. If this early return is
+ // removed, we'll need to handle mPositionArea explicitly.
return true;
}
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-001.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-001.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>Tests that scroll adjustments of implicitly anchored elements are applied correctly</title>
+<link rel="author" href="mailto:dshin@mozilla.com">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/">
+<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
+<style>
+.positioned {
+ position: fixed;
+ width: 100px;
+ height: 50px;
+ left: anchor(left);
+ top: anchor(bottom);
+ background: green;
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
+.container {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ background: red;
+ overflow: hidden;
+}
+
+.filler {
+ width: 1px;
+ height: 50px;
+}
+
+.anchor {
+ width: 100px;
+ height: 50px;
+ background: green;
+ border: none;
+ padding: 0;
+}
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div class=positioned popover id="popover"></div>
+<div id=s class=container>
+ <div class=filler></div>
+ <button id=b class=anchor popovertarget="popover"></button>
+ <div class=filler></div>
+</div>
+<script>
+b.click();
+s.scrollTop = 50;
+document.documentElement.classList.remove('reftest-wait');
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-002.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-implicit-002.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<title>Tests that scroll adjustments of implicitly anchored elements are applied correctly</title>
+<link rel="author" href="mailto:dshin@mozilla.com">
+<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/">
+<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
+<style>
+.positioned {
+ position: absolute;
+ width: 100px;
+ height: 50px;
+ left: anchor(left);
+ top: anchor(bottom);
+ background: green;
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
+.container {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ background: red;
+}
+
+.scroller {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+}
+
+.filler {
+ width: 1px;
+ height: 50px;
+}
+
+.anchor {
+ width: 100px;
+ height: 50px;
+ background: green;
+ border: none;
+ padding: 0;
+}
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<div class=container>
+ <div id=s class=scroller>
+ <div class=filler></div>
+ <button id=b class=anchor popovertarget="popover"></button>
+ <div class=filler></div>
+ </div>
+ <div class=positioned popover id="popover"></div>
+</div>
+<script>
+b.click();
+s.scrollTop = 50;
+document.documentElement.classList.remove('reftest-wait');
+</script>
+</html>