Bug 1870783 part 5: Expose DETAILS and DETAILS_FOR relations for popovers and their invokers. r=eeejay

Even when there is a valid popovertarget, per the spec, these relations must not be exposed in certain cases.
This requires some special case code.
The code for calculating the DETAILS relation for popover invokers has been encapsulated in its own function.
We also use this to validate the reverse DETAILS_FOR relation on popovers.

Normally, when pushing cached relations to the parent process, we use IDRefsIterator so that we don't push implicit reverse relations.
For popover invokers, this wouldn't validate the other conditions.
To avoid duplicate special case code in RemoteAccessible, we use RelationByType for the DETAILS relation instead of IDRefsIterator when pushing the cache.
This leverages the LocalAccessible special case code for popover invokers.
This is fine for the DETAILS relation because nothing ever exposes an implicit reverse DETAILS relation.

Differential Revision: https://phabricator.services.mozilla.com/D199954
This commit is contained in:
James Teh 2024-02-12 06:24:16 +00:00
Родитель f2b94d8123
Коммит de58806e45
3 изменённых файлов: 138 добавлений и 7 удалений

Просмотреть файл

@ -1491,6 +1491,11 @@ void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
}
if (aAttribute == nsGkAtoms::popovertarget) {
mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
return;
}
if (aAttribute == nsGkAtoms::alt &&
!nsAccUtils::HasARIAAttr(elm, nsGkAtoms::aria_label) &&
!elm->HasAttr(nsGkAtoms::aria_labelledby)) {
@ -2021,6 +2026,30 @@ nsIContent* LocalAccessible::GetAtomicRegion() const {
return atomic.EqualsLiteral("true") ? loopContent : nullptr;
}
LocalAccessible* LocalAccessible::GetPopoverTargetDetailsRelation() const {
dom::Element* targetEl = mContent->GetEffectivePopoverTargetElement();
if (!targetEl) {
return nullptr;
}
LocalAccessible* targetAcc = mDoc->GetAccessible(targetEl);
if (!targetAcc) {
return nullptr;
}
// Even if the popovertarget is valid, there are a few cases where we must not
// expose it via the details relation.
if (const nsAttrValue* actionVal =
Elm()->GetParsedAttr(nsGkAtoms::popovertargetaction)) {
if (static_cast<PopoverTargetAction>(actionVal->GetEnumValue()) ==
PopoverTargetAction::Hide) {
return nullptr;
}
}
if (targetAcc->NextSibling() == this || targetAcc->PrevSibling() == this) {
return nullptr;
}
return targetAcc;
}
Relation LocalAccessible::RelationByType(RelationType aType) const {
if (!HasOwnContent()) return Relation();
@ -2301,13 +2330,35 @@ Relation LocalAccessible::RelationByType(RelationType aType) const {
case RelationType::CONTAINING_APPLICATION:
return Relation(ApplicationAcc());
case RelationType::DETAILS:
return Relation(
new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_details));
case RelationType::DETAILS: {
if (mContent->IsElement() &&
mContent->AsElement()->HasAttr(nsGkAtoms::aria_details)) {
return Relation(
new IDRefsIterator(mDoc, mContent, nsGkAtoms::aria_details));
}
if (LocalAccessible* target = GetPopoverTargetDetailsRelation()) {
return Relation(target);
}
return Relation();
}
case RelationType::DETAILS_FOR:
return Relation(
case RelationType::DETAILS_FOR: {
Relation rel(
new RelatedAccIterator(mDoc, mContent, nsGkAtoms::aria_details));
RelatedAccIterator invokers(mDoc, mContent, nsGkAtoms::popovertarget);
while (Accessible* invoker = invokers.Next()) {
// We should only expose DETAILS_FOR if DETAILS was exposed on the
// invoker. However, DETAILS exposure on popover invokers is
// conditional.
LocalAccessible* popoverTarget =
invoker->AsLocal()->GetPopoverTargetDetailsRelation();
if (popoverTarget) {
MOZ_ASSERT(popoverTarget == this);
rel.AppendTarget(invoker);
}
}
return rel;
}
case RelationType::ERRORMSG:
return Relation(
@ -3848,11 +3899,16 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
dom::HTMLLabelElement::FromNode(mContent)) {
rel.AppendTarget(mDoc, labelEl->GetControl());
}
} else if (data.mType == RelationType::DETAILS) {
// We need to use RelationByType for details because it might include
// popovertarget. Nothing exposes an implicit reverse details
// relation, so using RelationByType here is fine.
rel = RelationByType(RelationType::DETAILS);
} else {
// We use an IDRefsIterator here instead of calling RelationByType
// directly because we only want to cache explicit relations. Implicit
// relations will be computed and stored separately in the parent
// process.
// relations (e.g. LABEL_FOR exposed on the target of aria-labelledby)
// will be computed and stored separately in the parent process.
rel.AppendIter(new IDRefsIterator(mDoc, mContent, relAtom));
}

Просмотреть файл

@ -1009,6 +1009,8 @@ class LocalAccessible : public nsISupports, public Accessible {
* OOP iframe docs and tab documents.
*/
nsIFrame* FindNearestAccessibleAncestorFrame();
LocalAccessible* GetPopoverTargetDetailsRelation() const;
};
NS_DEFINE_STATIC_IID_ACCESSOR(LocalAccessible, NS_ACCESSIBLE_IMPL_IID)

Просмотреть файл

@ -291,3 +291,76 @@ addAccessibleTask(
},
{ chrome: true, topLevel: true }
);
/**
* Test details relations on popovers and their invokers.
*/
addAccessibleTask(
`
<button id="hide" popovertarget="popover" popovertargetaction="hide">hide</button>
<button id="toggle1" popovertarget="popover">toggle1</button>
<button id="toggle2">toggle2</button>
<button id="toggleSibling">toggleSibling</button>
<div id="popover" popover>popover</div>
<div id="details">details</div>
`,
async function testPopover(browser, docAcc) {
// The popover is hidden, so nothing should be referring to it.
const hide = findAccessibleChildByID(docAcc, "hide");
await testCachedRelation(hide, RELATION_DETAILS, []);
const toggle1 = findAccessibleChildByID(docAcc, "toggle1");
await testCachedRelation(toggle1, RELATION_DETAILS, []);
const toggle2 = findAccessibleChildByID(docAcc, "toggle2");
await testCachedRelation(toggle2, RELATION_DETAILS, []);
const toggleSibling = findAccessibleChildByID(docAcc, "toggleSibling");
await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
info("Showing popover");
let shown = waitForEvent(EVENT_SHOW, "popover");
toggle1.doAction(0);
const popover = (await shown).accessible;
await testCachedRelation(toggle1, RELATION_DETAILS, popover);
// toggle2 shouldn't have a details relation because it doesn't have a
// popovertarget.
await testCachedRelation(toggle2, RELATION_DETAILS, []);
// hide shouldn't have a details relation because its action is hide.
await testCachedRelation(hide, RELATION_DETAILS, []);
// toggleSibling shouldn't have a details relation because it is a sibling
// of the popover.
await testCachedRelation(toggleSibling, RELATION_DETAILS, []);
await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
info("Setting toggle2 popovertargetaction");
await invokeSetAttribute(browser, "toggle2", "popovertarget", "popover");
await testCachedRelation(toggle2, RELATION_DETAILS, popover);
await testCachedRelation(popover, RELATION_DETAILS_FOR, [toggle1, toggle2]);
info("Removing toggle2 popovertarget");
await invokeSetAttribute(browser, "toggle2", "popovertarget", null);
await testCachedRelation(toggle2, RELATION_DETAILS, []);
await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
info("Setting aria-details on toggle1");
await invokeSetAttribute(browser, "toggle1", "aria-details", "details");
const details = findAccessibleChildByID(docAcc, "details");
// aria-details overrides popover.
await testCachedRelation(toggle1, RELATION_DETAILS, details);
await testCachedRelation(popover, RELATION_DETAILS_FOR, []);
info("Removing aria-details from toggle1");
await invokeSetAttribute(browser, "toggle1", "aria-details", null);
await testCachedRelation(toggle1, RELATION_DETAILS, popover);
await testCachedRelation(popover, RELATION_DETAILS_FOR, toggle1);
info("Hiding popover");
let hidden = waitForEvent(EVENT_HIDE, popover);
toggle1.doAction(0);
// The relations between toggle1 and popover are removed when popover shuts
// down. However, this doesn't cause a cache update notification. Therefore,
// to avoid timing out in testCachedRelation, we must wait for a hide event
// first.
await hidden;
await testCachedRelation(toggle1, RELATION_DETAILS, []);
},
{ chrome: false, topLevel: true }
);