Bug 1887800 part 2: Support the UIA LabeledBy property. r=morgan

Differential Revision: https://phabricator.services.mozilla.com/D208852
This commit is contained in:
James Teh 2024-05-03 11:08:46 +00:00
parent 29645bd082
commit d48615b61b
3 changed files with 125 additions and 0 deletions

View File

@ -91,3 +91,53 @@ addUiaTask(
await testUiaRelationArray("none", "FlowsTo", []);
}
);
/**
* Test the LabeledBy property.
*/
addUiaTask(
`
<label id="label">label</label>
<input id="input" aria-labelledby="label">
<label id="wrappingLabel">
<input id="wrappedInput" value="wrappedInput">
<p id="wrappingLabelP">wrappingLabel</p>
</label>
<button id="button" aria-labelledby="label">content</button>
<button id="noLabel">noLabel</button>
`,
async function testLabeledBy() {
await definePyVar("doc", `getDocUia()`);
// input's LabeledBy should be label's text leaf.
let result = await runPython(`
input = findUiaByDomId(doc, "input")
label = findUiaByDomId(doc, "label")
labelLeaf = uiaClient.RawViewWalker.GetFirstChildElement(label)
return uiaClient.CompareElements(input.CurrentLabeledBy, labelLeaf)
`);
ok(result, "input has correct LabeledBy");
// wrappedInput's LabeledBy should be wrappingLabelP's text leaf.
result = await runPython(`
wrappedInput = findUiaByDomId(doc, "wrappedInput")
wrappingLabelP = findUiaByDomId(doc, "wrappingLabelP")
wrappingLabelLeaf = uiaClient.RawViewWalker.GetFirstChildElement(wrappingLabelP)
return uiaClient.CompareElements(wrappedInput.CurrentLabeledBy, wrappingLabelLeaf)
`);
ok(result, "wrappedInput has correct LabeledBy");
// button has aria-labelledby, but UIA prohibits LabeledBy on buttons.
ok(
!(await runPython(
`bool(findUiaByDomId(doc, "button").CurrentLabeledBy)`
)),
"button has no LabeledBy"
);
ok(
!(await runPython(
`bool(findUiaByDomId(doc, "noLabel").CurrentLabeledBy)`
)),
"noLabel has no LabeledBy"
);
},
// The IA2 -> UIA proxy doesn't expose LabeledBy properly.
{ uiaEnabled: true, uiaDisabled: false }
);

View File

@ -22,7 +22,9 @@
#include "MsaaRootAccessible.h"
#include "nsAccessibilityService.h"
#include "nsAccUtils.h"
#include "nsIAccessiblePivot.h"
#include "nsTextEquivUtils.h"
#include "Pivot.h"
#include "Relation.h"
#include "RootAccessible.h"
@ -60,6 +62,29 @@ static bool IsRadio(Accessible* aAcc) {
return r == roles::RADIOBUTTON || r == roles::RADIO_MENU_ITEM;
}
// Used to search for a text leaf descendant for the LabeledBy property.
class LabelTextLeafRule : public PivotRule {
public:
virtual uint16_t Match(Accessible* aAcc) override {
if (aAcc->IsTextLeaf()) {
nsAutoString name;
aAcc->Name(name);
if (name.IsEmpty() || name.EqualsLiteral(" ")) {
// An empty or white space text leaf isn't useful as a label.
return nsIAccessibleTraversalRule::FILTER_IGNORE;
}
return nsIAccessibleTraversalRule::FILTER_MATCH;
}
if (!nsTextEquivUtils::HasNameRule(aAcc, eNameFromSubtreeIfReqRule)) {
// Don't descend into things that can't be used as label content; e.g.
// text boxes.
return nsIAccessibleTraversalRule::FILTER_IGNORE |
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
return nsIAccessibleTraversalRule::FILTER_IGNORE;
}
};
////////////////////////////////////////////////////////////////////////////////
// uiaRawElmProvider
////////////////////////////////////////////////////////////////////////////////
@ -602,6 +627,15 @@ uiaRawElmProvider::GetPropertyValue(PROPERTYID aPropertyId,
(acc->State() & states::FOCUSABLE) ? VARIANT_TRUE : VARIANT_FALSE;
return S_OK;
case UIA_LabeledByPropertyId:
if (Accessible* target = GetLabeledBy()) {
aPropertyValue->vt = VT_UNKNOWN;
RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(target);
uia.forget(&aPropertyValue->punkVal);
return S_OK;
}
break;
case UIA_LevelPropertyId:
aPropertyValue->vt = VT_I4;
aPropertyValue->lVal = acc->GroupPosition().level;
@ -1262,6 +1296,46 @@ SAFEARRAY* uiaRawElmProvider::AccRelationsToUiaArray(
return AccessibleArrayToUiaArray(targets);
}
Accessible* uiaRawElmProvider::GetLabeledBy() const {
// Per the UIA documentation, some control types should never get a value for
// the LabeledBy property.
switch (GetControlType()) {
case UIA_ButtonControlTypeId:
case UIA_CheckBoxControlTypeId:
case UIA_DataItemControlTypeId:
case UIA_MenuControlTypeId:
case UIA_MenuBarControlTypeId:
case UIA_RadioButtonControlTypeId:
case UIA_ScrollBarControlTypeId:
case UIA_SeparatorControlTypeId:
case UIA_StatusBarControlTypeId:
case UIA_TabItemControlTypeId:
case UIA_TextControlTypeId:
case UIA_ToolBarControlTypeId:
case UIA_ToolTipControlTypeId:
case UIA_TreeItemControlTypeId:
return nullptr;
}
Accessible* acc = Acc();
MOZ_ASSERT(acc);
// Even when LabeledBy is supported, it can only return a single "static text"
// element.
Relation rel = acc->RelationByType(RelationType::LABELLED_BY);
LabelTextLeafRule rule;
while (Accessible* target = rel.Next()) {
// If target were a text leaf, we should return that, but that shouldn't be
// possible because only an element (not a text node) can be the target of a
// relation.
MOZ_ASSERT(!target->IsTextLeaf());
Pivot pivot(target);
if (Accessible* leaf = pivot.Next(target, rule)) {
return leaf;
}
}
return nullptr;
}
SAFEARRAY* a11y::AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs) {
if (aAccs.IsEmpty()) {
// The UIA documentation is unclear about this, but the UIA client

View File

@ -191,6 +191,7 @@ class uiaRawElmProvider : public IAccessibleEx,
bool HasSelectionItemPattern();
SAFEARRAY* AccRelationsToUiaArray(
std::initializer_list<RelationType> aTypes) const;
Accessible* GetLabeledBy() const;
};
SAFEARRAY* AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs);