commit e31a35d4ea410835cf5613f288cbe6fa3a73309c
parent c67896dd6201b7bed70a412d2146929c2ddc9d49
Author: Eitan Isaacson <eitan@monotonous.org>
Date: Tue, 30 Sep 2025 17:19:31 +0000
Bug 1987945 - P3: Support details relation in anchored targets in remote accessibles. r=Jamie,layout-anchor-positioning-reviewers,layout-reviewers,emilio,firefox-style-system-reviewers
The change notification here is a bit stilted.. we need an explicit
notification when removing targets or anchors immediately before reflow
so we can collect associated frames and update the relations on their
associated accessibles.
When anchors/targets are added, we need to wait till after reflow in
order to get the frames that are resolved.
Differential Revision: https://phabricator.services.mozilla.com/D264637
Diffstat:
12 files changed, 198 insertions(+), 8 deletions(-)
diff --git a/accessible/base/CacheConstants.h b/accessible/base/CacheConstants.h
@@ -91,6 +91,8 @@ static constexpr RelationData kRelationTypeAtoms[] = {
RelationType::DETAILS_FOR},
{nsGkAtoms::popovertarget, nullptr, RelationType::DETAILS,
RelationType::DETAILS_FOR},
+ {nsGkAtoms::target, nullptr, RelationType::DETAILS,
+ RelationType::DETAILS_FOR},
{nsGkAtoms::aria_errormessage, nullptr, RelationType::ERRORMSG,
RelationType::ERRORMSG_FOR},
};
diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp
@@ -674,6 +674,48 @@ void nsAccessibilityService::NotifyOfDevPixelRatioChange(
}
}
+void nsAccessibilityService::NotifyAnchorPositionedRemoved(
+ mozilla::PresShell* aPresShell, nsIFrame* aFrame) {
+ DocAccessible* document = aPresShell->GetDocAccessible();
+ if (!document) {
+ return;
+ }
+
+ nsIFrame* anchorFrame =
+ nsCoreUtils::GetAnchorForPositionedFrame(aPresShell, aFrame);
+ if (!anchorFrame) {
+ return;
+ }
+
+ if (LocalAccessible* anchorAcc =
+ document->GetAccessible(anchorFrame->GetContent())) {
+ document->QueueCacheUpdate(anchorAcc, CacheDomain::Relations);
+ }
+}
+
+void nsAccessibilityService::NotifyAnchorRemoved(mozilla::PresShell* aPresShell,
+ nsIFrame* aFrame) {
+ DocAccessible* document = aPresShell->GetDocAccessible();
+ if (!document) {
+ return;
+ }
+
+ nsIFrame* positionedFrame =
+ nsCoreUtils::GetPositionedFrameForAnchor(aPresShell, aFrame);
+ if (!positionedFrame) {
+ return;
+ }
+
+ if (LocalAccessible* positionedAcc =
+ document->GetAccessible(positionedFrame->GetContent())) {
+ // If the anchor was removed, its positioned element may now have a 1:1
+ // relation with another anchor, and they would get a description a11y
+ // relation. So we need to go one level deeper here and refresh the cache of
+ // any potential anchors that remain on the positioned element.
+ document->RefreshAnchorRelationCacheForTarget(positionedAcc);
+ }
+}
+
void nsAccessibilityService::NotifyAttrElementWillChange(
mozilla::dom::Element* aElement, nsAtom* aAttr) {
mozilla::dom::Document* doc = aElement->OwnerDoc();
diff --git a/accessible/base/nsAccessibilityService.h b/accessible/base/nsAccessibilityService.h
@@ -267,6 +267,23 @@ class nsAccessibilityService final : public mozilla::a11y::DocManager,
int32_t aAppUnitsPerDevPixel);
/**
+ * Notify accessibility that an anchor positioned frame is
+ * about to be removed. This gives us a chance to update cached relations
+ * before the reflow where we will lose references to the anchor and won't be
+ * able to refresh its accessible's cache.
+ */
+ void NotifyAnchorPositionedRemoved(mozilla::PresShell* aPresShell,
+ nsIFrame* aFrame);
+
+ /**
+ * Notify accessibility that an anchor frame is about to be removed. This
+ * gives us a chance to update cached relations before the reflow where the
+ * anchor will be lost and we won't be able to refresh the accessible cache of
+ * prior relations.
+ */
+ void NotifyAnchorRemoved(mozilla::PresShell* aPresShell, nsIFrame* aFrame);
+
+ /**
* Notify accessibility that an element explicitly set for an attribute is
* about to change. See dom::Element::ExplicitlySetAttrElement.
*/
diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp
@@ -23,6 +23,7 @@
#include "TreeWalker.h"
#include "xpcAccessibleDocument.h"
+#include "AnchorPositioningUtils.h"
#include "nsIDocShell.h"
#include "mozilla/dom/Document.h"
#include "nsPIDOMWindow.h"
@@ -464,6 +465,27 @@ void DocAccessible::QueueCacheUpdateForDependentRelations(
}
QueueCacheUpdate(relatedAcc, CacheDomain::Relations);
}
+
+ if (nsIFrame* anchorFrame = nsCoreUtils::GetAnchorForPositionedFrame(
+ mPresShell, aAcc->GetFrame())) {
+ // If this accessible is anchored, retrieve the anchor and update its
+ // relations.
+ if (LocalAccessible* anchorAcc = GetAccessible(anchorFrame->GetContent())) {
+ if (!mInsertedAccessibles.Contains(anchorAcc)) {
+ QueueCacheUpdate(anchorAcc, CacheDomain::Relations);
+ }
+ }
+ }
+
+ if (nsIFrame* positionedFrame = nsCoreUtils::GetPositionedFrameForAnchor(
+ mPresShell, aAcc->GetFrame())) {
+ // If this accessible is an anchor, retrieve the positioned frame and
+ // refresh the cache on all its anchors.
+ if (LocalAccessible* targetAcc =
+ GetAccessible(positionedFrame->GetContent())) {
+ RefreshAnchorRelationCacheForTarget(targetAcc);
+ }
+ }
}
////////////////////////////////////////////////////////////////////////////////
@@ -3161,3 +3183,26 @@ bool DocAccessible::ProcessAnchorJump() {
mAnchorJumpElm = nullptr;
return true;
}
+
+void DocAccessible::RefreshAnchorRelationCacheForTarget(
+ LocalAccessible* aTarget) {
+ nsIFrame* frame = aTarget->GetFrame();
+ if (!frame || !frame->HasProperty(nsIFrame::AnchorPosReferences())) {
+ return;
+ }
+
+ AnchorPosReferenceData* referencedAnchors =
+ frame->GetProperty(nsIFrame::AnchorPosReferences());
+ for (auto& entry : *referencedAnchors) {
+ const auto& anchorName = entry.GetKey();
+ if (const nsIFrame* anchorFrame =
+ mPresShell->GetAnchorPosAnchor(anchorName, frame)) {
+ if (LocalAccessible* anchorAcc =
+ GetAccessible(anchorFrame->GetContent())) {
+ if (!mInsertedAccessibles.Contains(anchorAcc)) {
+ QueueCacheUpdate(anchorAcc, CacheDomain::Relations);
+ }
+ }
+ }
+ }
+}
diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h
@@ -415,6 +415,12 @@ class DocAccessible : public HyperTextAccessible,
void AttrElementWillChange(dom::Element* aElement, nsAtom* aAttr);
void AttrElementChanged(dom::Element* aElement, nsAtom* aAttr);
+ /**
+ * Given an accessible, check if it is anchored to other frames, and
+ * refresh the cache on each of those frames' accessibles.
+ */
+ void RefreshAnchorRelationCacheForTarget(LocalAccessible* aTarget);
+
protected:
virtual ~DocAccessible();
diff --git a/accessible/generic/LocalAccessible.cpp b/accessible/generic/LocalAccessible.cpp
@@ -1505,9 +1505,15 @@ void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
mDoc->QueueCacheUpdate(this, CacheDomain::Value);
}
+ if (aAttribute == nsGkAtoms::aria_details) {
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ // If an aria-details attribute is added or removed from an anchored
+ // accessible, it will change the validity of its anchor's relation.
+ mDoc->RefreshAnchorRelationCacheForTarget(this);
+ }
+
if (aAttribute == nsGkAtoms::aria_controls ||
aAttribute == nsGkAtoms::aria_flowto ||
- aAttribute == nsGkAtoms::aria_details ||
aAttribute == nsGkAtoms::aria_errormessage) {
mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
}
@@ -4251,6 +4257,11 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
if (LocalAccessible* target = GetPopoverTargetDetailsRelation()) {
rel.AppendTarget(target);
}
+ } else if (relAtom == nsGkAtoms::target) {
+ if (LocalAccessible* target =
+ GetAnchorPositionTargetDetailsRelation()) {
+ rel.AppendTarget(target);
+ }
} else {
MOZ_ASSERT_UNREACHABLE("Unknown details relAtom");
}
@@ -4433,6 +4444,22 @@ void LocalAccessible::MaybeQueueCacheUpdateForStyleChanges() {
mDoc->QueueCacheUpdate(this, CacheDomain::Style);
}
+ if (mOldComputedStyle->StyleDisplay()->mAnchorName !=
+ newStyle->StyleDisplay()->mAnchorName) {
+ // Anchor name changes can affect the result of
+ // details relations.
+ mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
+ }
+
+ if (mOldComputedStyle->MaybeAnchorPosReferencesDiffer(newStyle)) {
+ // Refresh the cache for details on current target (ie. the old style)
+ mDoc->RefreshAnchorRelationCacheForTarget(this);
+ // Refresh the cache for details on new target asynchronously after the
+ // next layout tick for new style.
+ mDoc->Controller()->ScheduleNotification<DocAccessible>(
+ mDoc, &DocAccessible::RefreshAnchorRelationCacheForTarget, this);
+ }
+
nsAutoCString oldPosition, newPosition;
mOldComputedStyle->GetComputedPropertyValue(eCSSProperty_position,
oldPosition);
diff --git a/accessible/ipc/RemoteAccessible.cpp b/accessible/ipc/RemoteAccessible.cpp
@@ -1204,6 +1204,14 @@ Relation RemoteAccessible::RelationByType(RelationType aType) const {
if (auto maybeIds = aAcc->mCachedFields->GetAttribute<nsTArray<uint64_t>>(
data.mAtom)) {
+ if (data.mAtom == nsGkAtoms::target) {
+ if (!maybeIds->IsEmpty() &&
+ !nsAccUtils::IsValidDetailsTargetForAnchor(
+ aAcc->mDoc->GetAccessible(maybeIds->ElementAt(0)), aAcc)) {
+ continue;
+ }
+ }
+
// Relations can have several cached attributes in order of precedence,
// if one is found we use it.
return maybeIds;
diff --git a/accessible/tests/browser/relations/browser_anchor_positioning.js b/accessible/tests/browser/relations/browser_anchor_positioning.js
@@ -127,7 +127,7 @@ addAccessibleTask(
await testNoDetailsRelations(btn2, target2);
},
- { chrome: true, topLevel: false }
+ { chrome: true, topLevel: true }
);
/**
@@ -162,7 +162,7 @@ addAccessibleTask(
await testDetailsRelations(siblingBtn, siblingTarget);
},
- { chrome: true, topLevel: false }
+ { chrome: true, topLevel: true }
);
/**
@@ -218,7 +218,7 @@ addAccessibleTask(
await testDetailsRelations(ownerBtn, ownedTarget);
}
},
- { chrome: true, topLevel: false }
+ { chrome: true, topLevel: true }
);
/**
@@ -357,7 +357,7 @@ addAccessibleTask(
await invokeSetAttributeAndTick(browser, "multiAnchor-btn1", "hidden");
await testNoDetailsRelations(multiAnchorBtn2, multiAnchorTarget);
},
- { chrome: true, topLevel: false }
+ { chrome: true, topLevel: true }
);
/**
@@ -387,7 +387,7 @@ addAccessibleTask(
const tooltipTarget = findAccessibleChildByID(docAcc, "tooltip-target");
await testNoDetailsRelations(btn, tooltipTarget);
},
- { chrome: true, topLevel: false }
+ { chrome: true, topLevel: true }
);
/**
@@ -509,7 +509,7 @@ addAccessibleTask(
);
await testDetailsRelations(btnTargetsetdetails, targetTargetsetdetails);
},
- { chrome: true, topLevel: false }
+ { chrome: true, topLevel: true }
);
/**
@@ -565,5 +565,5 @@ addAccessibleTask(
);
await testNoDetailsRelations(anchor1, target);
},
- { chrome: true, topLevel: false }
+ { chrome: true, topLevel: true }
);
diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
@@ -12126,6 +12126,12 @@ void PresShell::RemoveAnchorPosAnchor(const nsAtom* aName, nsIFrame* aFrame) {
return; // Nothing to remove.
}
+#ifdef ACCESSIBILITY
+ if (nsAccessibilityService* accService = GetAccService()) {
+ accService->NotifyAnchorRemoved(this, aFrame);
+ }
+#endif
+
auto& anchorArray = entry.Data();
// XXX: Once the implementation is more complete,
diff --git a/layout/base/PresShell.h b/layout/base/PresShell.h
@@ -774,6 +774,11 @@ class PresShell final : public nsStubDocumentObserver,
}
inline void RemoveAnchorPosPositioned(nsIFrame* aFrame) {
+#ifdef ACCESSIBILITY
+ if (nsAccessibilityService* accService = GetAccService()) {
+ accService->NotifyAnchorPositionedRemoved(this, aFrame);
+ }
+#endif
mAnchorPosPositioned.RemoveElement(aFrame);
}
diff --git a/layout/style/ComputedStyle.cpp b/layout/style/ComputedStyle.cpp
@@ -457,4 +457,34 @@ bool ComputedStyle::HasAnchorPosReference() const {
});
}
+// XXX: This is a broad-stroke method to return true if the referenced anchors
+// in two computed style may differ. Since we do not get the actual anchor
+// names, we do not know if the difference is just a position/size/margin change
+// or if indeed the anchors changed. So we will get false positives here, hence
+// "maybe".
+bool ComputedStyle::MaybeAnchorPosReferencesDiffer(
+ const ComputedStyle* aOther) const {
+ if (!HasAnchorPosReference() || !aOther->HasAnchorPosReference()) {
+ return true;
+ }
+
+ const auto* pos = StylePosition();
+ const auto* otherPos = aOther->StylePosition();
+ if (pos->mOffset != otherPos->mOffset || pos->mWidth != otherPos->mWidth ||
+ pos->mHeight != otherPos->mHeight ||
+ pos->mMinWidth != otherPos->mMinWidth ||
+ pos->mMinHeight != otherPos->mMinHeight ||
+ pos->mMaxWidth != otherPos->mMaxWidth ||
+ pos->mMaxHeight != otherPos->mMaxHeight ||
+ pos->mPositionAnchor != otherPos->mPositionAnchor) {
+ return true;
+ }
+
+ if (StyleMargin()->mMargin != aOther->StyleMargin()->mMargin) {
+ return true;
+ }
+
+ return false;
+}
+
} // namespace mozilla
diff --git a/layout/style/ComputedStyle.h b/layout/style/ComputedStyle.h
@@ -196,6 +196,8 @@ class ComputedStyle {
bool HasAnchorPosReference() const;
+ bool MaybeAnchorPosReferencesDiffer(const ComputedStyle* aOther) const;
+
ComputedStyle* GetCachedInheritingAnonBoxStyle(
PseudoStyleType aPseudoType) const {
MOZ_ASSERT(PseudoStyle::IsInheritingAnonBox(aPseudoType));