commit d4a74323746149b7c28582969751fc37f68c768a
parent f2dc3d6423d33cc4162c326e1dd424a312a06372
Author: David Shin <dshin@mozilla.com>
Date: Fri, 28 Nov 2025 19:46:06 +0000
Bug 1999954: Force reflow for anchor positioned frames whose anchors are fixed/chain anchor positioned. r=layout-anchor-positioning-reviewers,layout-reviewers,jwatt,emilio
Differential Revision: https://phabricator.services.mozilla.com/D274349
Diffstat:
3 files changed, 177 insertions(+), 19 deletions(-)
diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
@@ -11616,16 +11616,31 @@ static nsTArray<AffectedAnchorGroup> FindAnchorsAffectedByScroll(
static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor(
const PresShell* aPresShell,
const nsTArray<AffectedAnchorGroup>& aAffectedAnchors,
- const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData) {
+ const nsIFrame* aPositioned, const AnchorPosReferenceData& aReferenceData,
+ const nsIFrame** aResolvedDefaultAnchor) {
MOZ_ASSERT(aPositioned->IsAbsolutelyPositioned(),
"Anchor positioned frame is not absolutely positioned?");
+ if (aResolvedDefaultAnchor) {
+ *aResolvedDefaultAnchor = nullptr;
+ }
+
if (aReferenceData.IsEmpty()) {
return Nothing{};
}
- if (!aReferenceData.mDefaultAnchorName) {
+ const auto* defaultAnchorName = aReferenceData.mDefaultAnchorName.get();
+ if (!defaultAnchorName) {
+ return Nothing{};
+ }
+
+ const auto* defaultAnchor =
+ aPresShell->GetAnchorPosAnchor(defaultAnchorName, aPositioned);
+ if (!defaultAnchor) {
return Nothing{};
}
+ if (aResolvedDefaultAnchor) {
+ *aResolvedDefaultAnchor = defaultAnchor;
+ }
const auto compensatingForScroll = aReferenceData.CompensatingForScrollAxes();
if (compensatingForScroll.isEmpty()) {
@@ -11639,25 +11654,12 @@ static Maybe<const AffectedAnchor&> FindScrollCompensatedAnchor(
};
// Find the relevant default anchor.
- const auto* defaultAnchorName =
- AnchorPositioningUtils::GetUsedAnchorName(aPositioned, nullptr);
- nsIFrame const* defaultAnchor = nullptr;
for (const auto& group : aAffectedAnchors) {
- if (defaultAnchorName &&
- !group.mAnchorName->Equals(defaultAnchorName->GetUTF16String(),
- defaultAnchorName->GetLength())) {
+ if (group.mAnchorName != defaultAnchorName) {
// Default anchor has a name, and it's different from this affected
// anchor group.
continue;
}
- if (!defaultAnchor) {
- defaultAnchor =
- aPresShell->GetAnchorPosAnchor(defaultAnchorName, aPositioned);
- if (!defaultAnchor) {
- // Default anchor not valid for this positioned frame.
- return Nothing{};
- }
- }
const auto& anchors = group.mFrames;
// Find the affected anchor that not only matches in name, but in actual
// frame.
@@ -11690,6 +11692,41 @@ static bool CheckOverflow(nsIFrame* aPositioned,
return hasFallbacks && overflows;
}
+// HACK(dshin, Bug 1999954): This is a workaround. While we try to lay out
+// against the scroll-ignored position of an anchor, sticky and chain anchor
+// positioned frames actually end up containing scroll offset in their position.
+// Additionally, scroll offset collection does not do any special handling for
+// such frames (Which is impossible unless we can cleanly separate the
+// scroll-ignored position).
+// For now, we detect such frames and just trigger a reflow.
+// Bug 2002789 tracks the proper fix.
+static bool AnchorIsStickyOrChainedToScrollCompensatedAnchor(
+ const nsIFrame* aAnchor) {
+ if (!aAnchor) {
+ return false;
+ }
+
+ if (aAnchor->IsStickyPositioned()) {
+ return true;
+ }
+
+ if (aAnchor->StylePosition()->mPositionAnchor.IsNone()) {
+ // Not anchored, or anchored but not scroll compensated.
+ return false;
+ }
+
+ const auto* referenceData =
+ aAnchor->GetProperty(nsIFrame::AnchorPosReferences());
+ if (!referenceData) {
+ return false;
+ }
+
+ // Theoretically, we should look at the entire anchor chain to see if this
+ // anchor will be affected by scroll compensation. However, that does not seem
+ // worth it - A long anchor chain seems like an edge case anyway.
+ return !referenceData->CompensatingForScrollAxes().isEmpty();
+}
+
// https://drafts.csswg.org/css-anchor-position-1/#default-scroll-shift
void PresShell::UpdateAnchorPosForScroll(
const ScrollContainerFrame* aScrollContainer) {
@@ -11716,8 +11753,9 @@ void PresShell::UpdateAnchorPosForScroll(
if (!referenceData) {
continue;
}
+ const nsIFrame* defaultAnchor = nullptr;
const auto scrollDependency = FindScrollCompensatedAnchor(
- this, affectedAnchors, positioned, *referenceData);
+ this, affectedAnchors, positioned, *referenceData, &defaultAnchor);
const bool offsetChanged = [&]() {
if (!scrollDependency) {
return false;
@@ -11745,8 +11783,18 @@ void PresShell::UpdateAnchorPosForScroll(
}();
const bool cbScrolls =
positioned->GetParent() == aScrollContainer->GetScrolledFrame();
- if (offsetChanged || cbScrolls) {
- if (CheckOverflow(positioned, *referenceData)) {
+ // HACK(dshin): Check if this positioned frame is anchoring to a sticky or
+ // another anchor positioned frame, even if we may not be scroll
+ // compensating against it. Such frames, even when in the same scroll
+ // container, as the positioned element, don't (always) scroll with the
+ // scroll container. Also see comment for
+ // `AnchorIsStickyOrChainedToScrollCompensatedAnchor`.
+ const bool anchorIsStickyOrScrollCompensatedAnchor =
+ defaultAnchor &&
+ AnchorIsStickyOrChainedToScrollCompensatedAnchor(defaultAnchor);
+ if (offsetChanged || cbScrolls || anchorIsStickyOrScrollCompensatedAnchor) {
+ if (CheckOverflow(positioned, *referenceData) ||
+ anchorIsStickyOrScrollCompensatedAnchor) {
#ifdef ACCESSIBILITY
if (nsAccessibilityService* accService = GetAccService()) {
accService->NotifyAnchorPositionedScrollUpdate(this, positioned);
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-composited-scrolling-paint-001-ref.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-composited-scrolling-paint-001-ref.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1999954">
+<style>
+.abs-cb {
+ width: 100px;
+ height: 100px;
+ position: relative;
+}
+
+.anchor {
+ anchor-name: --a;
+ width: 50px;
+ height: 50px;
+ background: blue;
+}
+
+.chain {
+ width: 25px;
+ height: 25px;
+ background: pink;
+ position: absolute;
+ left: 50px;
+ top: 50px;
+}
+
+.positioned {
+ width: 25px;
+ height: 25px;
+ background: yellow;
+ position: absolute;
+ left: 75px;
+ top: 75px;
+}
+</style>
+<div class=abs-cb>
+ <div class=anchor></div>
+ <div class=chain></div>
+ <div class=positioned></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-composited-scrolling-paint-001.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scroll-composited-scrolling-paint-001.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html class=reftest-wait>
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1999954">
+<link rel="match" href="anchor-scroll-composited-scrolling-paint-001-ref.html">
+<script src="/common/reftest-wait.js"></script>
+<style>
+.abs-cb {
+ width: 100px;
+ height: 100px;
+ position: relative;
+}
+
+.scroller {
+ overflow: scroll;
+ scrollbar-width: none;
+ width: 100%;
+ height: 100%;
+}
+
+.anchor {
+ anchor-name: --a;
+ width: 50px;
+ height: 50px;
+ background: blue;
+}
+
+.chain {
+ width: 25px;
+ height: 25px;
+ background: pink;
+ position: absolute;
+ position-anchor: --a;
+ anchor-name: --b;
+ left: anchor(right);
+ top: anchor(bottom);
+}
+
+.filler {
+ width: 1px;
+ height: 50px;
+}
+
+.positioned {
+ width: 25px;
+ height: 25px;
+ background: yellow;
+ position: absolute;
+ position-anchor: --b;
+ left: anchor(right);
+ top: anchor(bottom);
+ position-visibility: always;
+}
+</style>
+<div class=abs-cb>
+ <div class=scroller>
+ <div id=s class=scroller>
+ <div class=filler></div>
+ <div class=anchor></div>
+ <div class=filler></div>
+ </div>
+ <div class=chain></div>
+ </div>
+ <div class=positioned></div>
+</div>
+<script>
+window.addEventListener("TestRendered", () => {
+ s.scrollTop = 50;
+ takeScreenshot();
+});
+</script>
+</html>