Bug 1795221: Implement LINKS_TO relation as a tree traversal r=Jamie

Differential Revision: https://phabricator.services.mozilla.com/D159451
This commit is contained in:
Morgan Rae Reschenberg 2022-10-27 20:32:18 +00:00
Родитель dbeec6c2f2
Коммит 771a9f1a94
7 изменённых файлов: 90 добавлений и 18 удалений

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

@ -81,7 +81,6 @@ static constexpr RelationData kRelationTypeAtoms[] = {
Some(RelationType::DESCRIPTION_FOR)},
{nsGkAtoms::aria_flowto, nullptr, RelationType::FLOWS_TO,
Some(RelationType::FLOWS_FROM)},
{nsGkAtoms::link, nullptr, RelationType::LINKS_TO, Nothing()},
};
} // namespace a11y

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

@ -652,7 +652,7 @@ uint16_t PivotRadioNameRule::Match(Accessible* aAcc) {
}
if (remote->IsHTMLRadioButton()) {
nsString currName = remote->GetCachedHTMLRadioNameAttribute();
nsString currName = remote->GetCachedHTMLNameAttribute();
if (!currName.IsEmpty() && mName.Equals(currName)) {
result |= nsIAccessibleTraversalRule::FILTER_MATCH;
}
@ -660,3 +660,18 @@ uint16_t PivotRadioNameRule::Match(Accessible* aAcc) {
return result;
}
// MustPruneSameDocRule
uint16_t MustPruneSameDocRule::Match(Accessible* aAcc) {
if (!aAcc) {
return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (nsAccUtils::MustPrune(aAcc) || aAcc->IsOuterDoc()) {
return nsIAccessibleTraversalRule::FILTER_MATCH |
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
return nsIAccessibleTraversalRule::FILTER_MATCH;
}

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

@ -136,6 +136,16 @@ class PivotRadioNameRule : public PivotRule {
const nsString& mName;
};
/**
* This rule doesn't search iframes. Subtrees that should be
* pruned by way of nsAccUtils::MustPrune are also not searched.
*/
class MustPruneSameDocRule : public PivotRule {
public:
virtual uint16_t Match(Accessible* aAcc) override;
};
} // namespace a11y
} // namespace mozilla

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

@ -1369,7 +1369,6 @@ void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
if (aAttribute == nsGkAtoms::href) {
mDoc->QueueCacheUpdate(this, CacheDomain::Value);
mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
}
if (aAttribute == nsGkAtoms::aria_controls ||
@ -1432,6 +1431,13 @@ void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
if (aAttribute == nsGkAtoms::accesskey) {
mDoc->QueueCacheUpdate(this, CacheDomain::Actions);
}
if (aAttribute == nsGkAtoms::name &&
(mContent && mContent->IsHTMLElement(nsGkAtoms::a))) {
// If an anchor's name changed, it's possible a LINKS_TO relation
// also changed. Push a cache update for Relations.
mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
}
}
void LocalAccessible::ARIAGroupPosition(int32_t* aLevel, int32_t* aSetSize,
@ -3618,18 +3624,21 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
}
if (aCacheDomain & CacheDomain::Relations && mContent) {
if (IsHTMLRadioButton()) {
if (IsHTMLRadioButton() ||
(mContent->IsElement() &&
mContent->AsElement()->IsHTMLElement(nsGkAtoms::a))) {
// HTML radio buttons with the same name should be grouped
// and returned together when their MEMBER_OF relation is
// requested. We cache the name attribute, if it exists, here.
// requested. Computing LINKS_TO also requires we cache `name` on
// anchor elements.
nsString name;
mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::name, name);
if (!name.IsEmpty()) {
fields->SetAttribute(nsGkAtoms::radioLabel, std::move(name));
fields->SetAttribute(nsGkAtoms::attributeName, std::move(name));
} else if (aUpdateType != CacheUpdateType::Initial) {
// It's possible we used to have a name and it's since been
// removed. Send a delete entry.
fields->SetAttribute(nsGkAtoms::radioLabel, DeleteEntry());
fields->SetAttribute(nsGkAtoms::attributeName, DeleteEntry());
}
}
@ -3646,10 +3655,6 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
dom::HTMLLabelElement::FromNode(mContent)) {
rel.AppendTarget(mDoc, labelEl->GetControl());
}
} else if (data.mType == RelationType::LINKS_TO) {
// This has no implicit relation, so it's safe to call RelationByType
// directly.
rel = RelationByType(RelationType::LINKS_TO);
} else {
// We use an IDRefsIterator here instead of calling RelationByType
// directly because we only want to cache explicit relations. Implicit

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

@ -693,6 +693,43 @@ Relation RemoteAccessibleBase<Derived>::RelationByType(
return Relation();
}
if (aType == RelationType::LINKS_TO && Role() == roles::LINK) {
Pivot p = Pivot(mDoc);
nsString href;
Value(href);
if (!href.IsEmpty()) {
// `Value` will give us the entire URL, we're only interested in the ID
// after the hash. Split that part out.
for (auto s : href.Split('#')) {
href = s;
}
MustPruneSameDocRule rule;
Accessible* nameMatch = nullptr;
for (Accessible* match = p.Next(mDoc, rule); match;
match = p.Next(match, rule)) {
nsString currID;
match->DOMNodeID(currID);
MOZ_ASSERT(match->IsRemote());
if (href.Equals(currID)) {
return Relation(match->AsRemote());
}
if (!nameMatch) {
nsString currName = match->AsRemote()->GetCachedHTMLNameAttribute();
if (match->TagName() == nsGkAtoms::a && href.Equals(currName)) {
// If we find an element with a matching ID, we should return
// that, but if we don't we should return the first anchor with
// a matching name. To avoid doing two traversals, store the first
// name match here.
nameMatch = match;
}
}
}
return nameMatch ? Relation(nameMatch->AsRemote()) : Relation();
}
return Relation();
}
// Handle ARIA tree, treegrid parent/child relations. Each of these cases
// relies on cached group info. To find the parent of an accessible, use the
// unified conceptual parent.
@ -1007,15 +1044,13 @@ RemoteAccessibleBase<Derived>::GetCachedARIAAttributes() const {
}
template <class Derived>
nsString RemoteAccessibleBase<Derived>::GetCachedHTMLRadioNameAttribute()
const {
nsString RemoteAccessibleBase<Derived>::GetCachedHTMLNameAttribute() const {
if (mCachedFields) {
if (auto maybeName =
mCachedFields->GetAttribute<nsString>(nsGkAtoms::radioLabel)) {
mCachedFields->GetAttribute<nsString>(nsGkAtoms::attributeName)) {
return *maybeName;
}
}
return nsString();
}

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

@ -348,7 +348,7 @@ class RemoteAccessibleBase : public Accessible, public HyperTextAccessibleBase {
RefPtr<const AccAttributes> GetCachedTextAttributes();
RefPtr<const AccAttributes> GetCachedARIAAttributes() const;
nsString GetCachedHTMLRadioNameAttribute() const;
nsString GetCachedHTMLNameAttribute() const;
virtual HyperTextAccessibleBase* AsHyperTextBase() override {
return IsHyperText() ? static_cast<HyperTextAccessibleBase*>(this)

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

@ -3,6 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
requestLongerTimeout(2);
/* import-globals-from ../../mochitest/relations.js */
loadScripts({ name: "relations.js", dir: MOCHITESTS_DIR });
@ -358,21 +359,28 @@ addAccessibleTask(
*/
addAccessibleTask(
`
<a id="link" href="#item">
<a id="link" href="#item">a</a>
<div id="item">hello</div>
<div id="item2">world</div>`,
<div id="item2">world</div>
<a id="link2" href="#anchor">b</a>
<a id="namedLink" name="anchor">c</a>`,
async function(browser, accDoc) {
const link = findAccessibleChildByID(accDoc, "link");
const link2 = findAccessibleChildByID(accDoc, "link2");
const namedLink = findAccessibleChildByID(accDoc, "namedLink");
const item = findAccessibleChildByID(accDoc, "item");
const item2 = findAccessibleChildByID(accDoc, "item2");
await testCachedRelation(link, RELATION_LINKS_TO, item);
await testCachedRelation(link2, RELATION_LINKS_TO, namedLink);
await invokeContentTask(browser, [], () => {
content.document.getElementById("link").href = "";
content.document.getElementById("namedLink").name = "newName";
});
await testCachedRelation(link, RELATION_LINKS_TO, null);
await testCachedRelation(link2, RELATION_LINKS_TO, null);
await invokeContentTask(browser, [], () => {
content.document.getElementById("link").href = "#item2";