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:
@@ -637,3 +637,13 @@ int32_t nsAccUtils::FindARIAAttrValueIn(dom::Element* aElement,
|
|||||||
}
|
}
|
||||||
return index;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ class nsAccUtils {
|
|||||||
const nsAtom* aName,
|
const nsAtom* aName,
|
||||||
AttrArray::AttrValuesArray* aValues,
|
AttrArray::AttrValuesArray* aValues,
|
||||||
nsCaseTreatment aCaseSensitive);
|
nsCaseTreatment aCaseSensitive);
|
||||||
|
|
||||||
|
static bool IsEditableARIACombobox(const LocalAccessible* aAccessible);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace a11y
|
} // namespace a11y
|
||||||
|
|||||||
@@ -2470,6 +2470,13 @@ void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
|
|||||||
logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
|
logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
|
||||||
#endif
|
#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 =
|
nsTArray<RefPtr<LocalAccessible>>* owned =
|
||||||
mARIAOwnsHash.GetOrInsertNew(aOwner);
|
mARIAOwnsHash.GetOrInsertNew(aOwner);
|
||||||
|
|
||||||
|
|||||||
@@ -2281,14 +2281,32 @@ Relation LocalAccessible::RelationByType(RelationType aType) const {
|
|||||||
return Relation();
|
return Relation();
|
||||||
}
|
}
|
||||||
|
|
||||||
case RelationType::CONTROLLED_BY:
|
case RelationType::CONTROLLED_BY: {
|
||||||
return Relation(new RelatedAccIterator(Document(), mContent,
|
Relation rel(new RelatedAccIterator(Document(), mContent,
|
||||||
nsGkAtoms::aria_controls));
|
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: {
|
case RelationType::CONTROLLER_FOR: {
|
||||||
Relation rel(new AssociatedElementsIterator(mDoc, mContent,
|
Relation rel(new AssociatedElementsIterator(mDoc, mContent,
|
||||||
nsGkAtoms::aria_controls));
|
nsGkAtoms::aria_controls));
|
||||||
rel.AppendIter(new HTMLOutputIterator(Document(), mContent));
|
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;
|
return rel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4080,11 +4098,17 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
|
|||||||
dom::HTMLLabelElement::FromNode(mContent)) {
|
dom::HTMLLabelElement::FromNode(mContent)) {
|
||||||
rel.AppendTarget(mDoc, labelEl->GetControl());
|
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
|
// We need to use RelationByType for details because it might include
|
||||||
// popovertarget. Nothing exposes an implicit reverse details
|
// popovertarget. Nothing exposes an implicit reverse details
|
||||||
// relation, so using RelationByType here is fine.
|
// 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 {
|
} else {
|
||||||
// We use an AssociatedElementsIterator here instead of calling
|
// We use an AssociatedElementsIterator here instead of calling
|
||||||
// RelationByType directly because we only want to cache explicit
|
// RelationByType directly because we only want to cache explicit
|
||||||
|
|||||||
@@ -622,8 +622,8 @@ addAccessibleTask(
|
|||||||
async function (browser, accDoc) {
|
async function (browser, accDoc) {
|
||||||
const p = findAccessibleChildByID(accDoc, "p");
|
const p = findAccessibleChildByID(accDoc, "p");
|
||||||
const textbox = findAccessibleChildByID(accDoc, "textbox");
|
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");
|
isnot(getAccessibleDOMNodeID(p.lastChild), "btn", "'p' owns relocated btn");
|
||||||
is(textbox.value, "Hello");
|
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");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user