tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit d1276e3d41983f9e7da6fdf40ec49c9e2b8976f0
parent ec3475dc469dfc5416bb2159b26a5c7ee9cfe05e
Author: Jari Jalkanen <jjalkanen@mozilla.com>
Date:   Tue,  2 Dec 2025 19:29:07 +0000

Bug 1987086 - Handle anchor scopes with display: contents. r=layout-anchor-positioning-reviewers,layout-reviewers,dshin

Rebased version of D264040, with the test moved to WPT and my comments
fixed.

Co-authored-by: Emilio Cobos Álvarez <emilio@crisal.io>

Differential Revision: https://phabricator.services.mozilla.com/D274795

Diffstat:
Mlayout/base/AnchorPositioningUtils.cpp | 37++++++++++++++++++++-----------------
Atesting/web-platform/tests/css/css-anchor-position/anchor-scope-display-contents.tentative.html | 283+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 303 insertions(+), 17 deletions(-)

diff --git a/layout/base/AnchorPositioningUtils.cpp b/layout/base/AnchorPositioningUtils.cpp @@ -61,40 +61,43 @@ bool IsAnchorInScopeForPositionedElement(const nsAtom* aName, aPositionedFrame->GetParent()->GetContent(); auto getAnchorPosNearestScope = - [&positionedContainingBlockContent]( - const nsAtom* aName, const nsIFrame* aFrame) -> const nsIContent* { + [&](const nsAtom* aName, const nsIFrame* aFrame) -> const nsIContent* { // We need to traverse the DOM, not the frame tree, since `anchor-scope` // may be present on elements with `display: contents` (in which case its // frame is in the `::before` list and won't be found by walking the frame // tree parent chain). - for (const nsIContent* cp = aFrame->GetContent(); + for (nsIContent* cp = aFrame->GetContent(); cp && cp != positionedContainingBlockContent; cp = cp->GetFlattenedTreeParentElementForStyle()) { - // TODO: The case when no frame is generated needs to be - // handled, e.g. `display: contents`, see bug 1987086. - const nsIFrame* f = cp->GetPrimaryFrame(); - if (!f) { - continue; - } + const auto* anchorScope = [&]() -> const StyleAnchorScope* { + const nsIFrame* f = nsLayoutUtils::GetStyleFrame(cp); + if (MOZ_LIKELY(f)) { + return &f->StyleDisplay()->mAnchorScope; + } + if (cp->AsElement()->IsDisplayContents()) { + const auto* style = + Servo_Element_GetMaybeOutOfDateStyle(cp->AsElement()); + MOZ_ASSERT(style); + return &style->StyleDisplay()->mAnchorScope; + } + return nullptr; + }(); - const StyleAnchorScope& anchorScope = f->StyleDisplay()->mAnchorScope; - if (anchorScope.IsNone()) { + if (!anchorScope || anchorScope->IsNone()) { continue; } - if (anchorScope.IsAll()) { + if (anchorScope->IsAll()) { return cp; } - MOZ_ASSERT(anchorScope.IsIdents()); - for (const StyleAtom& ident : anchorScope.AsIdents().AsSpan()) { - const auto* id = ident.AsAtom(); - if (aName->Equals(id->GetUTF16String(), id->GetLength())) { + MOZ_ASSERT(anchorScope->IsIdents()); + for (const StyleAtom& ident : anchorScope->AsIdents().AsSpan()) { + if (aName == ident.AsAtom()) { return cp; } } } - return nullptr; }; diff --git a/testing/web-platform/tests/css/css-anchor-position/anchor-scope-display-contents.tentative.html b/testing/web-platform/tests/css/css-anchor-position/anchor-scope-display-contents.tentative.html @@ -0,0 +1,283 @@ +<!DOCTYPE html> +<title>CSS Anchor Positioning: Basic anchor-scope behavior when scope element has display:contents property</title> +<link rel="help" href="https://drafts.csswg.org/css-anchor-position-1/#anchor-scope"> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/12783"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<!-- This test is based on anchor-scope-basic.html --> +<style> + .scope-all { anchor-scope: all; display: contents; } + .scope-a { anchor-scope: --a; display: contents; } + .scope-b { anchor-scope: --b; ; display: contents; } + .scope-ab { anchor-scope: --a, --b; ; display: contents; } + + .anchor-a { anchor-name: --a; } + .anchor-b { anchor-name: --b; } + .anchor-ab { anchor-name: --a, --b; } + .anchor-a, .anchor-b, .anchor-ab { + background: skyblue; + height: 10px; + } + + .anchored-a { position-anchor: --a; } + .anchored-b { position-anchor: --b; } + .anchored-a, .anchored-b { + position: absolute; + top: anchor(bottom); + left: anchor(left); + width: 5px; + height: 5px; + background: coral; + } + + /* Containing block */ + main { + position: relative; + width: 100px; + height: 100px; + border: 1px solid black; + } +</style> +<script> + function inflate(t, template_element) { + t.add_cleanup(() => main.replaceChildren()); + main.append(template_element.content.cloneNode(true)); + } +</script> + +<main id=main> +</main> + +<template id=test_inclusive_subtree> + <div class="scope-a anchor-a"> <!--A--> + <div class=anchored-a></div> + </div> +</template> +<script> + test((t) => { + inflate(t, test_inclusive_subtree); + // In this case, anchor scope with display:contents is ignored. + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '0px'); + }, 'Can anchor to a name both defined and scoped by the same element'); +</script> + +<template id=test_skips_named_anchor_with_scope> + <div class="anchor-a"></div> + <div class="anchor-a"></div> + <div class="anchor-a"></div> <!--A--> + <div class="scope-a anchor-a"></div> + <div class=anchored-a></div> +</template> +<script> + test((t) => { + inflate(t, test_skips_named_anchor_with_scope); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '30px'); + }, 'Sibling can not anchor into anchor-scope, even when anchor-name present'); +</script> + +<template id=test_scope_all_common_ancestor> + <div class=scope-all> + <div class=anchor-a></div> + <div class=anchor-a></div> + <div class=anchor-a></div> + <div class=anchor-a></div><!--A--> + <div class=anchored-a></div> + </div> +</template> +<script> + test((t) => { + inflate(t, test_scope_all_common_ancestor); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '40px'); + }, 'anchor-scope:all on common ancestor'); +</script> + +<template id=test_scope_named_common_ancestor> + <div class=scope-a> + <div class=anchor-a></div> + <div class=anchor-a></div> + <div class=anchor-a></div> + <div class=anchor-a></div><!--A--> + <div class=anchored-a></div> + </div> +</template> +<script> + test((t) => { + inflate(t, test_scope_named_common_ancestor); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '40px'); + }, 'anchor-scope:--a on common ancestor'); +</script> + +<template id=test_scope_all_sibling> + <div class=anchor-a></div> + <div class=anchor-a></div><!--A--> + <div class=scope-all> + <div class=anchor-a></div> + <div class=anchor-a></div> + </div> + <div class=anchored-a></div> +</template> +<script> + test((t) => { + inflate(t, test_scope_all_sibling); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '20px'); + }, 'anchor-scope:all on sibling'); +</script> + +<template id=test_scope_all_multiple> + <div class=anchor-b></div><!--B--> + <div class=anchor-a></div><!--A--> + <div class=scope-all> + <div class=anchor-b></div> + <div class=anchor-a></div> + </div> + <div class=anchored-a></div> + <div class=anchored-b></div> +</template> +<script> + test((t) => { + inflate(t, test_scope_all_multiple); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '20px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).top, '10px'); + }, 'anchor-scope:all scopes multiple names'); +</script> + +<template id=test_scope_ab> + <div class=anchor-b></div><!--B--> + <div class=anchor-a></div><!--A--> + <div class=scope-ab> + <div class=anchor-b></div> + <div class=anchor-a></div> + </div> + <div class=anchored-a></div> + <div class=anchored-b></div> +</template> +<script> + test((t) => { + inflate(t, test_scope_ab); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '20px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).top, '10px'); + }, 'anchor-scope:--a,--b scopes --a and --b'); +</script> + +<template id=test_scope_a> + <div class=anchor-b></div> + <div class=anchor-a></div><!--A--> + <div class=scope-a> + <div class=anchor-b></div> + <div class=anchor-ab></div><!--B--> + <div class=anchor-a></div> + </div> + <div class=anchored-a></div> + <div class=anchored-b></div> +</template> +<script> + test((t) => { + inflate(t, test_scope_a); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '20px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).top, '40px'); + }, 'anchor-scope:--a scopes only --a'); +</script> + +<template id=test_scope_b> + <div class=anchor-b></div><!--B--> + <div class=anchor-a></div> + <div class=scope-b> + <div class=anchor-a></div><!--A--> + <div class=anchor-b></div> + </div> + <div class=anchored-a></div> + <div class=anchored-b></div> +</template> +<script> + test((t) => { + inflate(t, test_scope_b); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '30px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).top, '10px'); + }, 'anchor-scope:--b scopes only --b'); +</script> + +<template id=test_out_of_flow_anchors> + <style> + .anchor-a, .anchor-b { + position: absolute; + width: 5px; + height: 5px; + } + </style> + <div class=anchor-b style='left:10px'></div> + <div class=anchor-a style='left:20px'></div><!--A--> + <div class=scope-a> + <div class=anchor-b style='left:30px'></div><!--B--> + <div class=anchor-a style='left:40px'></div> + </div> + <div class=anchored-a></div> + <div class=anchored-b></div> +</template> +<script> + test((t) => { + inflate(t, test_out_of_flow_anchors); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '5px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).left, '20px'); + + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).top, '5px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).left, '30px'); + }, 'anchor-scope:--a scopes only --a (out-of-flow anchors)'); +</script> + +<!-- Out-of-flow anchor within anchor-scope:--a --> +<template id=test_mixed_flow_anchors> + <style> + .abs { + position: absolute; + width: 5px; + height: 5px; + } + </style> + <div class=anchor-b></div> + <div class=anchor-a></div><!--A--> + <div class=scope-a> + <div class=anchor-b></div><!--B--> + <div class='anchor-a abs' style='top:50px'></div> + </div> + <div class=anchored-a></div> + <div class=anchored-b></div> +</template> +<script> + test((t) => { + inflate(t, test_mixed_flow_anchors); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '20px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).left, '0px'); + + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).top, '30px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).left, '0px'); + }, 'anchor-scope:--a scopes only --a (both out-of-flow and in-flow anchors)'); +</script> + +<!-- In-flow anchor within anchor-scope:--a --> +<template id=test_mixed_flow_anchors_reverse> + <style> + .abs { + position: absolute; + width: 5px; + height: 5px; + } + </style> + <div class=anchor-b></div> + <div class='anchor-a abs' style='top:50px'></div><!--A--> + <div class=scope-a> + <div class=anchor-b></div><!--B--> + <div class=anchor-a></div> + </div> + <div class=anchored-a></div> + <div class=anchored-b></div> +</template> +<script> + test((t) => { + inflate(t, test_mixed_flow_anchors_reverse); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).top, '55px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-a')).left, '0px'); + + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).top, '20px'); + assert_equals(getComputedStyle(main.querySelector('.anchored-b')).left, '0px'); + }, 'anchor-scope:--a scopes only --a (both out-of-flow and in-flow anchors, reverse)'); +</script>