Bug 1912520 - Morph aria-owns to controls relationship when container is combobox. r=Jamie

Differential Revision: https://phabricator.services.mozilla.com/D236931
This commit is contained in:
Eitan Isaacson
2025-02-07 00:47:55 +00:00
parent 3212d879b7
commit 7991e8091e
5 changed files with 85 additions and 6 deletions

View File

@@ -637,3 +637,13 @@ int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element* aElement,
}
return index;
}
bool nsAccUtils::IsEditableARIACombobox(const LocalAccessible* aAccessible) {
const nsRoleMapEntry* roleMap = aAccessible->ARIARoleMap();
if (!roleMap || roleMap->role != roles::EDITCOMBOBOX) {
return false;
}
return aAccessible->IsTextField() ||
aAccessible->Elm()->State().HasState(dom::ElementState::READWRITE);
}

View File

@@ -301,6 +301,8 @@ class nsAccUtils {
const nsAtom* aName,
AttrArray::AttrValuesArray* aValues,
nsCaseTreatment aCaseSensitive);
static bool IsEditableARIACombobox(const LocalAccessible* aAccessible);
};
} // namespace a11y

View File

@@ -2470,6 +2470,13 @@ void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
#endif
const nsRoleMapEntry* roleMap = aOwner->ARIARoleMap();
if (roleMap && roleMap->role == roles::EDITCOMBOBOX) {
// The READWRITE state of a combobox may sever aria-owns relations
// we fallback to "controls" relations.
QueueCacheUpdate(aOwner, CacheDomain::Relations);
}
nsTArray<RefPtr<LocalAccessible>>* owned =
mARIAOwnsHash.GetOrInsertNew(aOwner);

View File

@@ -2281,14 +2281,32 @@ Relation LocalAccessible::RelationByType(RelationType aType) const {
return Relation();
}
case RelationType::CONTROLLED_BY:
return Relation(new RelatedAccIterator(Document(), mContent,
nsGkAtoms::aria_controls));
case RelationType::CONTROLLED_BY: {
Relation rel(new RelatedAccIterator(Document(), mContent,
nsGkAtoms::aria_controls));
RelatedAccIterator owners(Document(), mContent, nsGkAtoms::aria_owns);
if (LocalAccessible* owner = owners.Next()) {
if (nsAccUtils::IsEditableARIACombobox(owner)) {
MOZ_ASSERT(!IsRelocated(),
"Child is not relocated to editable combobox");
rel.AppendTarget(owner);
}
}
return rel;
}
case RelationType::CONTROLLER_FOR: {
Relation rel(new AssociatedElementsIterator(mDoc, mContent,
nsGkAtoms::aria_controls));
rel.AppendIter(new HTMLOutputIterator(Document(), mContent));
if (nsAccUtils::IsEditableARIACombobox(this)) {
AssociatedElementsIterator iter(mDoc, mContent, nsGkAtoms::aria_owns);
while (Accessible* owned_child = iter.Next()) {
MOZ_ASSERT(!owned_child->AsLocal()->IsRelocated());
rel.AppendTarget(owned_child->AsLocal());
}
}
return rel;
}
@@ -4080,11 +4098,17 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
dom::HTMLLabelElement::FromNode(mContent)) {
rel.AppendTarget(mDoc, labelEl->GetControl());
}
} else if (data.mType == RelationType::DETAILS) {
} else if (data.mType == RelationType::DETAILS ||
data.mType == RelationType::CONTROLLER_FOR) {
// 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);
//
// We need to use RelationByType for controls because it might include
// failed aria-owned relocations or it may be an output element.
// Nothing exposes an implicit reverse controls relation, so using
// RelationByType here is fine.
rel = RelationByType(data.mType);
} else {
// We use an AssociatedElementsIterator here instead of calling
// RelationByType directly because we only want to cache explicit

View File

@@ -622,8 +622,8 @@ addAccessibleTask(
async function (browser, accDoc) {
const p = findAccessibleChildByID(accDoc, "p");
const textbox = findAccessibleChildByID(accDoc, "textbox");
testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0);
testStates(textbox, 0, EXT_STATE_EDITABLE, 0, 0);
isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
is(textbox.value, "Hello");
@@ -700,3 +700,39 @@ addAccessibleTask(
);
}
);
addAccessibleTask(
`
<div id="box" role="combobox"
aria-owns="listbox"
aria-expanded="true"
aria-haspopup="listbox"
aria-autocomplete="list"
contenteditable="true"></div>
<ul role="listbox" id="listbox">
<li role="option">apple</li>
<li role="option">peach</li>
</ul>
`,
async (browser, accDoc) => {
const combobox = findAccessibleChildByID(accDoc, "box");
const listbox = findAccessibleChildByID(accDoc, "listbox");
testStates(combobox, 0, EXT_STATE_EDITABLE, 0, 0);
is(combobox.childCount, 0, "combobox has no children");
await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, [listbox]);
await testCachedRelation(listbox, RELATION_CONTROLLED_BY, [combobox]);
let expectedEvents = Promise.all([
waitForStateChange(combobox, EXT_STATE_EDITABLE, false, true),
waitForEvent(EVENT_REORDER, accDoc),
]);
await invokeContentTask(browser, [], () => {
content.document.getElementById("box").contentEditable = false;
});
await expectedEvents;
await testCachedRelation(combobox, RELATION_CONTROLLER_FOR, []);
await testCachedRelation(listbox, RELATION_CONTROLLED_BY, []);
is(combobox.childCount, 1, "combobox has listbox");
}
);