Bug 2070171 - Clear last fallback properly when don't fit after fitting. r=dshin

If a fallback used to fit, but after fallback none of the positions fit,
we end up in the base position, but we didn't clear the last index
properly, so the fallback reflow kept triggering.

Move the clear to after the loop, and add a test that repros with or
without overlay scrollbars. Add also the test as a crashtest.

Pull request: https://github.com/mozilla-firefox/firefox/pull/362
This commit is contained in:
Emilio Cobos Álvarez
2026-09-09 14:18:12 +00:00
committed by ealvarez@mozilla.com
parent 841fffc3f2
commit c70773c334
3 changed files with 83 additions and 16 deletions
+8 -16
View File
@@ -2250,17 +2250,6 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
// didn't have any to try in the first place.
isOverflowingCB = !fits;
fallback.CommitCurrentFallback();
if (currentFallbackIndex.isNothing()) {
if (auto* prop = aKidFrame->GetProperty(
nsIFrame::LastSuccessfulPositionFallback())) {
// When the fallback list changes, we clear the recorded fallback data
// as per spec, so we shouldn't get there in this case.
MOZ_ASSERT(!fallbacks.IsEmpty(), "how?");
prop->mLastIndex.reset();
prop->mLastStyle = nullptr;
prop->mTriedAllFallbacks = isOverflowingCB;
}
}
break;
}
@@ -2349,11 +2338,14 @@ void AbsoluteContainingBlock::ReflowAbsoluteFrame(
}
}();
if (currentFallbackIndex) {
auto* lastSuccessfulPosition = aKidFrame->GetOrCreateDeletableProperty(
nsIFrame::LastSuccessfulPositionFallback());
// NOTE: We don't touch the last recorded index, that's done at resize
// observer time.
// NOTE: We don't touch the last recorded index, that's done at resize
// observer time.
auto* lastSuccessfulPosition =
currentFallbackIndex
? aKidFrame->GetOrCreateDeletableProperty(
nsIFrame::LastSuccessfulPositionFallback())
: aKidFrame->GetProperty(nsIFrame::LastSuccessfulPositionFallback());
if (lastSuccessfulPosition) {
lastSuccessfulPosition->mLastIndex = currentFallbackIndex;
lastSuccessfulPosition->mLastStyle = std::move(currentFallbackStyle);
lastSuccessfulPosition->mTriedAllFallbacks = isOverflowingCB;
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Anchor fallback retry loop</title>
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2070171">
<style>
html, body { margin: 0; }
#scroller { width: 400px; height: 300px; overflow: auto; }
#wrapper { position: relative; }
#tall { width: 50px; height: 301px; }
#anchor { anchor-name: --a; position: absolute; bottom: 0; left: 0;
width: 20px; height: 20px; }
#target { position: absolute; position-anchor: --a;
top: anchor(outside); left: 0;
position-try-fallbacks: flip-block;
width: 390px; height: 100px; }
</style>
<div id="scroller">
<div id="wrapper">
<div id="tall"></div>
<div id="anchor"></div>
<div id="target"></div>
</div>
</div>
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<title>CSS Anchor Positioning: no fallback fits after one fit in a previous layout</title>
<meta name="description" content="Tests that a layout where no position option fits, following a layout (without an intervening rendering update) where a fallback did fit, settles on the base position rather than retrying forever.">
<link rel="author" title="Mozilla" href="https://mozilla.org">
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="help" href="https://drafts.csswg.org/css-anchor-position/#fallback-apply">
<link rel="help" href="https://drafts.csswg.org/css-anchor-position/#last-successful-position-fallback">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="support/test-common.js"></script>
<style>
html, body { margin: 0; }
#container {
position: relative;
width: 400px;
height: 300px;
}
#anchor {
anchor-name: --a;
position: absolute;
bottom: 0;
left: 0;
width: 20px;
height: 20px;
}
#anchored {
position: absolute;
position-anchor: --a;
top: anchor(outside);
left: 0;
position-try-fallbacks: flip-block;
width: 390px;
height: 100px;
}
</style>
<div id="container">
<div id="anchor"></div>
<div id="anchored"></div>
</div>
<script>
promise_test(async () => {
// Base position (below the anchor) never fits; flip-block fits here.
assert_equals(anchored.offsetTop, 180, "flip-block fits initially");
// Now make flip-block not fit either, before the last successful position
// option has been recorded.
container.style.width = "380px";
assert_equals(anchored.offsetTop, 300, "base position when nothing fits");
await waitUntilNextAnimationFrame();
await waitUntilNextAnimationFrame();
assert_equals(anchored.offsetTop, 300, "base position after rendering");
}, "Nothing fits after a fallback fit in a previous layout without rendering");
</script>