tor-browser

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

commit 9cb7f8116308f12554ff18d16ed49c510f863aff
parent 2963f900bb6270f0e4d7c89731ce181345a45039
Author: Eitan Isaacson <eitan@monotonous.org>
Date:   Tue, 30 Sep 2025 17:19:31 +0000

Bug 1987945 - P1: Implement details relationship for CSS anchored local accessibles. r=Jamie,emilio

There are three functions that select and determine valid details
relationships.

1. nsCoreUtils::GetAnchorForPositionedFrame and nsCoreUtils::GetPositionedFrameForAnchor

These query layout and determine that there is only a single target
referencing and anchor and vice versa. We need to do this check in
layout because the a11y tree doesn't give us the full picture. For
example, one of two targets could be aria-hidden and it should make the
relationship with between the anchor and other target invalid.

2. LocalAccessible::GetAnchorPositionTargetDetailsRelation

This uses the functions above to get the target of an anchor, retrieve
its accessible and check that there are no explicit relationships
defined on the anchor (or details relationship for the target).

3. nsAccUtils::IsValidDetailsTargetForAnchor

This checks the a11y tree structure to make sure this is a valid
relationship. This is written to run also remotely so we don't need
to recalculate and cache relations on each tree mutation.

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

Diffstat:
Maccessible/base/nsAccUtils.cpp | 26++++++++++++++++++++++++++
Maccessible/base/nsAccUtils.h | 7+++++++
Maccessible/base/nsCoreUtils.cpp | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Maccessible/base/nsCoreUtils.h | 18++++++++++++++++++
Maccessible/generic/LocalAccessible.cpp | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Maccessible/generic/LocalAccessible.h | 2++
Maccessible/tests/browser/relations/browser.toml | 2++
Aaccessible/tests/browser/relations/browser_anchor_positioning.js | 569+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlayout/base/PresShell.h | 4++++
9 files changed, 781 insertions(+), 0 deletions(-)

diff --git a/accessible/base/nsAccUtils.cpp b/accessible/base/nsAccUtils.cpp @@ -651,3 +651,29 @@ bool nsAccUtils::IsEditableARIACombobox(const LocalAccessible* aAccessible) { return aAccessible->IsTextField() || aAccessible->Elm()->State().HasState(dom::ElementState::READWRITE); } + +bool nsAccUtils::IsValidDetailsTargetForAnchor(const Accessible* aTarget, + const Accessible* aAnchor) { + if (aAnchor->IsAncestorOf(aTarget)) { + // If the anchor is a parent of the target, the target is not valid + // relation. + return false; + } + + Accessible* nextSibling = aAnchor->NextSibling(); + if (nextSibling && nextSibling->IsTextLeaf()) { + nsAutoString text; + nextSibling->Name(text); + if (nsCoreUtils::IsWhitespaceString(text)) { + nextSibling = nextSibling->NextSibling(); + } + } + + if (nextSibling == aTarget) { + // If the target is the next sibling of the anchor (ignoring whitespace + // text nodes), the target is not a valid relation. + return false; + } + + return true; +} diff --git a/accessible/base/nsAccUtils.h b/accessible/base/nsAccUtils.h @@ -303,6 +303,13 @@ class nsAccUtils { nsCaseTreatment aCaseSensitive); static bool IsEditableARIACombobox(const LocalAccessible* aAccessible); + + /** + * Return true if the CSS positioned target of an anchor is a valid details + * related accessible. + */ + static bool IsValidDetailsTargetForAnchor(const Accessible* aDetails, + const Accessible* aTarget); }; } // namespace a11y diff --git a/accessible/base/nsCoreUtils.cpp b/accessible/base/nsCoreUtils.cpp @@ -29,6 +29,7 @@ #include "nsView.h" #include "nsGkAtoms.h" +#include "AnchorPositioningUtils.h" #include "nsComponentManagerUtils.h" #include "XULTreeElement.h" @@ -674,3 +675,70 @@ bool nsCoreUtils::IsTrimmedWhitespaceBeforeHardLineBreak(nsIFrame* aFrame) { nsIFrame::TrailingWhitespace::Trim); return text.mString.IsEmpty(); } + +nsIFrame* nsCoreUtils::GetAnchorForPositionedFrame( + const PresShell* aPresShell, const nsIFrame* aPositionedFrame) { + if (!aPositionedFrame || + !aPositionedFrame->Style()->HasAnchorPosReference()) { + return nullptr; + } + + const nsAtom* anchorName = nullptr; + AnchorPosReferenceData* referencedAnchors = + aPositionedFrame->GetProperty(nsIFrame::AnchorPosReferences()); + + if (!referencedAnchors) { + return nullptr; + } + + for (auto& entry : *referencedAnchors) { + if (entry.GetData().isNothing()) { + continue; + } + + if (anchorName && entry.GetKey() != anchorName) { + // Multiple anchors referenced. + return nullptr; + } + + anchorName = entry.GetKey(); + } + + return anchorName + ? aPresShell->GetAnchorPosAnchor(anchorName, aPositionedFrame) + : nullptr; +} + +nsIFrame* nsCoreUtils::GetPositionedFrameForAnchor( + const PresShell* aPresShell, const nsIFrame* aAnchorFrame) { + if (!aAnchorFrame) { + return nullptr; + } + + nsIFrame* positionedFrame = nullptr; + const auto* styleDisp = aAnchorFrame->StyleDisplay(); + if (styleDisp->HasAnchorName()) { + for (auto& name : styleDisp->mAnchorName.AsSpan()) { + for (nsIFrame* frame : aPresShell->GetAnchorPosPositioned()) { + // Bug 1990069: We need to iterate over all positioned frames in doc and + // check their referenced anchors because we don't store reverse mapping + // from anchor to positioned frame. + const auto* referencedAnchors = + frame->GetProperty(nsIFrame::AnchorPosReferences()); + const auto* data = referencedAnchors->Lookup(name.AsAtom()); + if (data && *data && data->ref().mOrigin) { + if (aAnchorFrame == + aPresShell->GetAnchorPosAnchor(name.AsAtom(), frame)) { + if (positionedFrame) { + // Multiple positioned frames reference this anchor. + return nullptr; + } + positionedFrame = frame; + } + } + } + } + } + + return positionedFrame; +} diff --git a/accessible/base/nsCoreUtils.h b/accessible/base/nsCoreUtils.h @@ -353,6 +353,24 @@ class nsCoreUtils { aContent->IsGeneratedContentContainerForAfter() || aContent->IsGeneratedContentContainerForMarker(); } + + /** + * Return the anchor frame for the given CSS positioned frame, or null if: + * 1. there is none, + * 2. there is more than one anchor, + * 3. or, there is one or more anchor used for sizing/margin only. + */ + static nsIFrame* GetAnchorForPositionedFrame( + const PresShell* aPresShell, const nsIFrame* aPositionedFrame); + + /** + * Return the CSS positioned frame for the given anchor frame, or null if: + * 1. there is none, + * 2. the anchor has more than one positioned frame, + * 3. or, there is one or more positioned frame using this anchor for sizing/margin only. + */ + static nsIFrame* GetPositionedFrameForAnchor(const PresShell* aPresShell, + const nsIFrame* aAnchorFrame); }; #endif diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp @@ -30,6 +30,7 @@ #include "mozilla/a11y/Role.h" #include "RootAccessible.h" #include "States.h" +#include "TextLeafAccessible.h" #include "TextLeafRange.h" #include "TextRange.h" #include "HTMLElementAccessibles.h" @@ -2193,6 +2194,69 @@ LocalAccessible* LocalAccessible::GetPopoverTargetDetailsRelation() const { return targetAcc; } +LocalAccessible* LocalAccessible::GetAnchorPositionTargetDetailsRelation() + const { + nsIFrame* positionedFrame = nsCoreUtils::GetPositionedFrameForAnchor( + mDoc->PresShellPtr(), GetFrame()); + if (!positionedFrame) { + return nullptr; + } + + if (!nsCoreUtils::GetAnchorForPositionedFrame(mDoc->PresShellPtr(), + positionedFrame)) { + // There is no reciprocal, 1:1, anchor for this positioned frame. + return nullptr; + } + + LocalAccessible* targetAcc = + mDoc->GetAccessible(positionedFrame->GetContent()); + + if (!targetAcc) { + return nullptr; + } + + if (targetAcc->Role() == roles::TOOLTIP) { + // A tooltip is never a valid target for details relation. + return nullptr; + } + + AssociatedElementsIterator describedby(mDoc, GetContent(), + nsGkAtoms::aria_describedby); + while (LocalAccessible* target = describedby.Next()) { + if (target == targetAcc) { + // An explicit description relation exists, so we don't want to create a + // details relation. + return nullptr; + } + } + + AssociatedElementsIterator labelledby(mDoc, GetContent(), + nsGkAtoms::aria_labelledby); + while (LocalAccessible* target = labelledby.Next()) { + if (target == targetAcc) { + // An explicit label relation exists, so we don't want to create a details + // relation. + return nullptr; + } + } + + dom::Element* anchorEl = targetAcc->Elm(); + if (anchorEl && anchorEl->HasAttr(nsGkAtoms::aria_details)) { + // If the anchor has an explicit aria-details attribute, then we don't want + // to create a details relation. + return nullptr; + } + + dom::Element* targetEl = Elm(); + if (targetEl && targetEl->HasAttr(nsGkAtoms::aria_details)) { + // If the target has an explicit aria-details attribute, then we don't want + // to create a details relation. + return nullptr; + } + + return targetAcc; +} + Relation LocalAccessible::RelationByType(RelationType aType) const { if (!HasOwnContent()) return Relation(); @@ -2508,6 +2572,12 @@ Relation LocalAccessible::RelationByType(RelationType aType) const { if (LocalAccessible* target = GetPopoverTargetDetailsRelation()) { return Relation(target); } + if (LocalAccessible* target = GetAnchorPositionTargetDetailsRelation()) { + if (nsAccUtils::IsValidDetailsTargetForAnchor(target, this)) { + return Relation(target); + } + } + return Relation(); } @@ -2538,6 +2608,21 @@ Relation LocalAccessible::RelationByType(RelationType aType) const { rel.AppendTarget(invoker); } } + + // Check early if the accessible is a tooltip. If so, it can never be a + // valid target for an anchor's details relation. + if (Role() != roles::TOOLTIP) { + if (nsIFrame* anchorFrame = nsCoreUtils::GetAnchorForPositionedFrame( + mDoc->PresShellPtr(), GetFrame())) { + LocalAccessible* anchorAcc = + mDoc->GetAccessible(anchorFrame->GetContent()); + if (anchorAcc->GetAnchorPositionTargetDetailsRelation() == this && + nsAccUtils::IsValidDetailsTargetForAnchor(this, anchorAcc)) { + rel.AppendTarget(anchorAcc); + } + } + } + return rel; } diff --git a/accessible/generic/LocalAccessible.h b/accessible/generic/LocalAccessible.h @@ -1048,6 +1048,8 @@ class LocalAccessible : public nsISupports, public Accessible { LocalAccessible* GetCommandForDetailsRelation() const; LocalAccessible* GetPopoverTargetDetailsRelation() const; + + LocalAccessible* GetAnchorPositionTargetDetailsRelation() const; }; //////////////////////////////////////////////////////////////////////////////// diff --git a/accessible/tests/browser/relations/browser.toml b/accessible/tests/browser/relations/browser.toml @@ -15,6 +15,8 @@ prefs = [ "layout.css.anchor-positioning.enabled=true" ] +["browser_anchor_positioning.js"] + ["browser_popover_and_command.js"] ["browser_relations_general.js"] diff --git a/accessible/tests/browser/relations/browser_anchor_positioning.js b/accessible/tests/browser/relations/browser_anchor_positioning.js @@ -0,0 +1,569 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function testDetailsRelations(anchor, target) { + await testCachedRelation(anchor, RELATION_DETAILS, target); + await testCachedRelation(target, RELATION_DETAILS_FOR, anchor); +} + +async function testNoDetailsRelations(anchor, target) { + await testCachedRelation(anchor, RELATION_DETAILS, []); + await testCachedRelation(target, RELATION_DETAILS_FOR, []); +} + +async function invokeContentTaskAndTick(browser, args, task) { + await invokeContentTask(browser, args, task); + + await invokeContentTask(browser, [], () => { + content.windowUtils.advanceTimeAndRefresh(100); + content.windowUtils.restoreNormalRefresh(); + }); +} + +async function invokeSetAttributeAndTick(browser, id, attr, attrValue) { + await invokeSetAttribute(browser, id, attr, attrValue); + await invokeContentTask(browser, [], () => { + content.windowUtils.advanceTimeAndRefresh(100); + content.windowUtils.restoreNormalRefresh(); + }); +} + +/** + * Test details relations for CSS explicit and implicit Anchor Positioning + */ +addAccessibleTask( + ` + <style> + #btn1 { + anchor-name: --btn1; + } + + #target1 { + position: absolute; + position-anchor: --btn1; + left: anchor(right); + bottom: anchor(top); + } + + #btn2 { + anchor-name: --btn2; + } + + #target2 { + position: absolute; + left: anchor(--btn2 right); + } + + #btn3 { + anchor-name: --btn3; + } + </style> + + <div id="target1">World</div> + <button id="btn1">Hello</button> + + <div id="target2">World</div> + <button id="btn2">Hello</button> + + <button id="btn3">No Target</button> + `, + async function testSimplePositionAnchors(browser, docAcc) { + info("Implicit anchor"); + const btn1 = findAccessibleChildByID(docAcc, "btn1"); + const target1 = findAccessibleChildByID(docAcc, "target1"); + await testDetailsRelations(btn1, target1); + + info("Make anchor invalid"); + await invokeContentTaskAndTick(browser, [], () => { + Object.assign(content.document.getElementById("btn1").style, { + "anchor-name": "--invalid", + }); + }); + + await testNoDetailsRelations(btn1, target1); + + info("Make anchor valid again"); + await invokeContentTaskAndTick(browser, [], () => { + Object.assign(content.document.getElementById("btn1").style, { + "anchor-name": "--btn1", + }); + }); + + await testDetailsRelations(btn1, target1); + + info("Assign target to different anchor"); + await invokeContentTaskAndTick(browser, [], () => { + Object.assign(content.document.getElementById("target1").style, { + "position-anchor": "--btn3", + }); + }); + + const btn3 = findAccessibleChildByID(docAcc, "btn3"); + await testDetailsRelations(btn3, target1); + await testCachedRelation(btn1, RELATION_DETAILS, []); + + info("Assign target to invalid anchor"); + await invokeContentTaskAndTick(browser, [], () => { + Object.assign(content.document.getElementById("target1").style, { + "position-anchor": "--invalid", + }); + }); + + await testNoDetailsRelations(btn3, target1); + + info("Explicit anchor"); + const btn2 = findAccessibleChildByID(docAcc, "btn2"); + const target2 = findAccessibleChildByID(docAcc, "target2"); + await testDetailsRelations(btn2, target2); + + await invokeContentTaskAndTick(browser, [], () => { + Object.assign(content.document.getElementById("target2").style, { + left: "0px", + }); + }); + + await testNoDetailsRelations(btn2, target2); + }, + { chrome: true, topLevel: false } +); + +/** + * Test no details relations for sibling target + */ +addAccessibleTask( + ` + <style> + #sibling-btn { + anchor-name: --sibling-btn; + } + + #sibling-target { + position: absolute; + position-anchor: --sibling-btn; + left: anchor(right); + bottom: anchor(top); + } + </style> + + <button id="sibling-btn">Hello</button> + <button id="intermediate-button" hidden>Cruel</button> + <div id="sibling-target">World</div> + `, + async function testSiblingPositionAnchor(browser, docAcc) { + info("Target is sibling after anchor, no relation"); + const siblingBtn = findAccessibleChildByID(docAcc, "sibling-btn"); + const siblingTarget = findAccessibleChildByID(docAcc, "sibling-target"); + await testNoDetailsRelations(siblingBtn, siblingTarget); + + await invokeSetAttributeAndTick(browser, "intermediate-button", "hidden"); + + await testDetailsRelations(siblingBtn, siblingTarget); + }, + { chrome: true, topLevel: false } +); + +/** + * Test no details relations parent anchor with child target + */ +addAccessibleTask( + ` + <style> + #parent-btn { + anchor-name: --parent-btn; + } + + #child-target { + position: absolute; + position-anchor: --parent-btn; + left: anchor(right); + bottom: anchor(top); + } + + #owner-btn { + anchor-name: --owner-btn; + } + + #owned-target { + position: absolute; + position-anchor: --owner-btn; + left: anchor(right); + bottom: anchor(top); + } + </style> + + <button id="parent-btn">Hello <div role="group"><div id="child-target">World</div></div></button> + + <div id="owned-target">World</div> + <button id="owner-btn" aria-owns="owned-target">Hello</button> + `, + async function testSiblingPositionAnchor(browser, docAcc) { + info("Target is child of anchor, no relation"); + const parentBtn = findAccessibleChildByID(docAcc, "parent-btn"); + const childTarget = findAccessibleChildByID(docAcc, "child-target"); + await testNoDetailsRelations(parentBtn, childTarget); + + if (!browser.isRemoteBrowser) { + // Bug 1989629: This doesn't work in e10s yet. + + info("Target is owned by anchor, no relation"); + const ownerBtn = findAccessibleChildByID(docAcc, "owner-btn"); + const ownedTarget = findAccessibleChildByID(docAcc, "owned-target"); + await testNoDetailsRelations(ownerBtn, ownedTarget); + + info("Remove aria owns, relation should be restored"); + await invokeSetAttributeAndTick(browser, "owner-btn", "aria-owns"); + await testDetailsRelations(ownerBtn, ownedTarget); + } + }, + { chrome: true, topLevel: false } +); + +/** + * Test no details relations for CSS anchor with multiple targets or targets with multiple anchors + */ +addAccessibleTask( + ` + <style> + #multiTarget-btn { + anchor-name: --multiTarget-btn; + } + + #multiTarget-target1 { + position: absolute; + position-anchor: --multiTarget-btn; + right: anchor(left); + bottom: anchor(top); + } + + #multiTarget-target2 { + position: absolute; + position-anchor: --multiTarget-btn; + left: anchor(right); + bottom: anchor(top); + } + + #multiTarget-target2.unanchored { + position-anchor: --invalid; + } + + #multiAnchor-btn1 { + anchor-name: --multiAnchor-btn1; + } + + #multiAnchor-btn2 { + anchor-name: --multiAnchor-btn2; + } + + #multiAnchor-target { + position: absolute; + left: anchor(--multiAnchor-btn1 right); + bottom: anchor(--multiAnchor-btn2 top); + right: anchor(--multiAnchor-btn2 left); + } + + #multiAnchor-target.unanchored { + left: 0px; + } + </style> + + <div id="multiTarget-target1">Cruel</div> + <div id="multiTarget-target2">World</div> + <button id="multiTarget-btn">Hello</button> + + <div id="multiAnchor-target">Hello</div> + <button id="multiAnchor-btn1">Cruel</button> + <button id="multiAnchor-btn2">World</button> + `, + async function testMultiplePositionAnchors(browser, docAcc) { + info("Multiple targets for one anchor"); + const multiTargetBtn = findAccessibleChildByID(docAcc, "multiTarget-btn"); + const multiTargetTarget1 = findAccessibleChildByID( + docAcc, + "multiTarget-target1" + ); + const multiTargetTarget2 = findAccessibleChildByID( + docAcc, + "multiTarget-target2" + ); + await testNoDetailsRelations(multiTargetBtn, multiTargetTarget1); + await testNoDetailsRelations(multiTargetBtn, multiTargetTarget2); + + info("Remove one target from anchor via styling"); + await invokeSetAttributeAndTick( + browser, + "multiTarget-target2", + "class", + "unanchored" + ); + + await testDetailsRelations(multiTargetBtn, multiTargetTarget1); + + info("Restore target styling"); + await invokeSetAttributeAndTick(browser, "multiTarget-target2", "class"); + + await testNoDetailsRelations(multiTargetBtn, multiTargetTarget2); + + info("Remove one target node completely"); + await invokeSetAttributeAndTick( + browser, + "multiTarget-target2", + "hidden", + "true" + ); + + await testDetailsRelations(multiTargetBtn, multiTargetTarget1); + + info("Add back target node"); + await invokeSetAttributeAndTick(browser, "multiTarget-target2", "hidden"); + + await testNoDetailsRelations(multiTargetBtn, multiTargetTarget1); + + info("Multiple anchors for one target"); + const multiAnchorBtn1 = findAccessibleChildByID(docAcc, "multiAnchor-btn1"); + const multiAnchorBtn2 = findAccessibleChildByID(docAcc, "multiAnchor-btn2"); + const multiAnchorTarget = findAccessibleChildByID( + docAcc, + "multiAnchor-target" + ); + await testNoDetailsRelations(multiAnchorBtn1, multiAnchorTarget); + await testNoDetailsRelations(multiAnchorBtn2, multiAnchorTarget); + + info("Remove one anchor via styling"); + await invokeSetAttributeAndTick( + browser, + "multiAnchor-target", + "class", + "unanchored" + ); + await testDetailsRelations(multiAnchorBtn2, multiAnchorTarget); + + info("Add back one anchor via styling"); + await invokeSetAttributeAndTick(browser, "multiAnchor-target", "class"); + await testNoDetailsRelations(multiAnchorBtn2, multiAnchorTarget); + + info("Remove one anchor node"); + await invokeSetAttributeAndTick( + browser, + "multiAnchor-btn1", + "hidden", + "true" + ); + await testDetailsRelations(multiAnchorBtn2, multiAnchorTarget); + + info("Add back anchor node"); + await invokeSetAttributeAndTick(browser, "multiAnchor-btn1", "hidden"); + await testNoDetailsRelations(multiAnchorBtn2, multiAnchorTarget); + }, + { chrome: true, topLevel: false } +); + +/** + * Test no details relations for tooltip target + */ +addAccessibleTask( + ` + <style> + #btn { + anchor-name: --btn; + } + + #tooltip-target { + position: absolute; + position-anchor: --btn; + left: anchor(right); + bottom: anchor(top); + } + </style> + + <div id="tooltip-target" role="tooltip">World</div> + <button id="btn">Hello</button> + `, + async function testTooltipPositionAnchor(browser, docAcc) { + info("Target is tooltip, no relation"); + const btn = findAccessibleChildByID(docAcc, "btn"); + const tooltipTarget = findAccessibleChildByID(docAcc, "tooltip-target"); + await testNoDetailsRelations(btn, tooltipTarget); + }, + { chrome: true, topLevel: false } +); + +/** + * Test no details relations for when explicit relations are set. + */ +addAccessibleTask( + ` + <style> + .target { + position: absolute; + left: anchor(right); + bottom: anchor(top); + } + + #btn-describedby { + anchor-name: --btn-describedby; + } + + #target-describedby { + position-anchor: --btn-describedby; + } + + #btn-labelledby { + anchor-name: --btn-labelledby; + } + + #target-labelledby { + position-anchor: --btn-labelledby; + } + + #btn-anchorsetdetails { + anchor-name: --btn-anchorsetdetails; + } + + #target-anchorsetdetails { + position-anchor: --btn-anchorsetdetails; + } + + #btn-targetsetdetails { + anchor-name: --btn-targetsetdetails; + } + + #target-targetsetdetails { + position-anchor: --btn-targetsetdetails; + } + + </style> + + <div id="target-describedby" class="target">World</div> + <button id="btn-describedby" aria-describedby="target-describedby">Hello</button> + + <div id="target-labelledby" class="target">World</div> + <button id="btn-labelledby" aria-labelledby="target-labelledby">Hello</button> + + <div id="target-anchorsetdetails" class="target">World</div> + <button id="btn-anchorsetdetails" aria-details="">Hello</button> + + <div id="target-targetsetdetails" aria-details="" class="target">World</div> + <button id="btn-targetsetdetails">Hello</button> + `, + async function testTooltipPositionAnchor(browser, docAcc) { + info("Test no details relations when explicit relations are set"); + const btnDescribedby = findAccessibleChildByID(docAcc, "btn-describedby"); + const targetDescribedby = findAccessibleChildByID( + docAcc, + "target-describedby" + ); + const btnLabelledby = findAccessibleChildByID(docAcc, "btn-labelledby"); + const targetLabelledby = findAccessibleChildByID( + docAcc, + "target-labelledby" + ); + const btnAnchorsetdetails = findAccessibleChildByID( + docAcc, + "btn-anchorsetdetails" + ); + const targetAnchorsetdetails = findAccessibleChildByID( + docAcc, + "target-anchorsetdetails" + ); + const btnTargetsetdetails = findAccessibleChildByID( + docAcc, + "btn-targetsetdetails" + ); + const targetTargetsetdetails = findAccessibleChildByID( + docAcc, + "target-targetsetdetails" + ); + + await testNoDetailsRelations(btnDescribedby, targetDescribedby); + await invokeSetAttributeAndTick( + browser, + "btn-describedby", + "aria-describedby" + ); + await testDetailsRelations(btnDescribedby, targetDescribedby); + + await testNoDetailsRelations(btnLabelledby, targetLabelledby); + await invokeSetAttributeAndTick( + browser, + "btn-labelledby", + "aria-labelledby" + ); + await testDetailsRelations(btnLabelledby, targetLabelledby); + + await testNoDetailsRelations(btnAnchorsetdetails, targetAnchorsetdetails); + await invokeSetAttributeAndTick( + browser, + "btn-anchorsetdetails", + "aria-details" + ); + await testDetailsRelations(btnAnchorsetdetails, targetAnchorsetdetails); + + await testNoDetailsRelations(btnTargetsetdetails, targetTargetsetdetails); + await invokeSetAttributeAndTick( + browser, + "target-targetsetdetails", + "aria-details" + ); + await testDetailsRelations(btnTargetsetdetails, targetTargetsetdetails); + }, + { chrome: true, topLevel: false } +); + +/** + * Test no details when anchor is used for sizing target only + */ +addAccessibleTask( + ` + <style> + #anchor1 { + anchor-name: --anchor1; + width: 200px; + } + + #anchor2 { + anchor-name: --anchor2; + height: 150px; + } + + #target { + position: absolute; + width: anchor-size(--anchor1 width); + } + + #target.positioned { + left: anchor(--anchor1 right); + } + + #target.anchor-height { + height: anchor-size(--anchor2 height); + } + </style> + + <div id="target">World</div> + <button id="anchor1">Hello</button> + <button id="anchor2">Cruel</button> + `, + async function testTooltipPositionAnchor(browser, docAcc) { + info("Target is tooltip, no relation"); + const anchor1 = findAccessibleChildByID(docAcc, "anchor1"); + const target = findAccessibleChildByID(docAcc, "target"); + await testNoDetailsRelations(anchor1, target); + + info("Use anchor for positioning as well"); + await invokeSetAttributeAndTick(browser, "target", "class", "positioned"); + await testDetailsRelations(anchor1, target); + + info("Use second anchor for sizing"); + await invokeSetAttributeAndTick( + browser, + "target", + "class", + "positioned anchor-height" + ); + await testNoDetailsRelations(anchor1, target); + }, + { chrome: true, topLevel: false } +); diff --git a/layout/base/PresShell.h b/layout/base/PresShell.h @@ -777,6 +777,10 @@ class PresShell final : public nsStubDocumentObserver, mAnchorPosPositioned.RemoveElement(aFrame); } + const nsTArray<nsIFrame*>& GetAnchorPosPositioned() const { + return mAnchorPosPositioned; + } + #ifdef MOZ_REFLOW_PERF void DumpReflows(); void CountReflows(const char* aName, nsIFrame* aFrame);