Bug 1878355: Restrict ARIA IDREF, node consultation in acc name traversals, r=Jamie

The Acc Name spec requires that UAs should only follow aria-labelledby and
aria-describedby IDREFs "if [...] the current node is not already part of an
ongoing aria-labelledby or aria-describedby traversal." It also requires
that "[e]ach node in the subtree is consulted only once." This revision
implements these rules and updates relevant tests, including removal of WPT
expected-fail designations. The concept of sInitiatorAcc has been expanded - we
now track all referenced accessibles. We also track whether we're in an
aria-labelledby or aria-describedby traversal.

Differential Revision: https://phabricator.services.mozilla.com/D209617
This commit is contained in:
Nathan LaPre 2024-05-20 20:16:36 +00:00
parent b38a3cc3f0
commit 6e696c39b1
6 changed files with 111 additions and 68 deletions

View file

@ -24,6 +24,21 @@ using namespace mozilla::a11y;
*/
static const Accessible* sInitiatorAcc = nullptr;
/*
* Track whether we're in an aria-describedby or aria-labelledby traversal. The
* browser should only follow those IDREFs, per the "LabelledBy" section of the
* AccName spec, "if [...] the current node is not already part of an ongoing
* aria-labelledby or aria-describedby traversal [...]"
*/
static bool sInAriaRelationTraversal = false;
/*
* Track the accessibles that we've consulted so far while computing the text
* alternative for an accessible. Per the Name From Content section of the Acc
* Name spec, "[e]ach node in the subtree is consulted only once."
*/
static nsTHashSet<const Accessible*> sReferencedAccs;
////////////////////////////////////////////////////////////////////////////////
// nsTextEquivUtils. Public.
@ -31,9 +46,16 @@ nsresult nsTextEquivUtils::GetNameFromSubtree(
const LocalAccessible* aAccessible, nsAString& aName) {
aName.Truncate();
if (sInitiatorAcc) return NS_OK;
if (sReferencedAccs.Contains(aAccessible)) {
return NS_OK;
}
// Remember the initiating accessible so we know when we've returned to it.
if (sReferencedAccs.IsEmpty()) {
sInitiatorAcc = aAccessible;
}
sReferencedAccs.Insert(aAccessible);
sInitiatorAcc = aAccessible;
if (GetRoleRule(aAccessible->Role()) == eNameFromSubtreeRule) {
// XXX: is it necessary to care the accessible is not a document?
if (aAccessible->IsContent()) {
@ -44,7 +66,13 @@ nsresult nsTextEquivUtils::GetNameFromSubtree(
}
}
sInitiatorAcc = nullptr;
// Once the text alternative computation is complete (i.e., once we've
// returned to the initiator acc), clear out the referenced accessibles and
// reset the initiator acc.
if (aAccessible == sInitiatorAcc) {
sReferencedAccs.Clear();
sInitiatorAcc = nullptr;
}
return NS_OK;
}
@ -52,6 +80,16 @@ nsresult nsTextEquivUtils::GetNameFromSubtree(
nsresult nsTextEquivUtils::GetTextEquivFromIDRefs(
const LocalAccessible* aAccessible, nsAtom* aIDRefsAttr,
nsAString& aTextEquiv) {
// If this is an aria-labelledby or aria-describedby traversal and we're
// already in such a traversal, or if we've already consulted the given
// accessible, early out.
const bool isAriaTraversal = aIDRefsAttr == nsGkAtoms::aria_labelledby ||
aIDRefsAttr == nsGkAtoms::aria_describedby;
if ((sInAriaRelationTraversal && isAriaTraversal) ||
sReferencedAccs.Contains(aAccessible)) {
return NS_OK;
}
aTextEquiv.Truncate();
nsIContent* content = aAccessible->GetContent();
@ -62,7 +100,18 @@ nsresult nsTextEquivUtils::GetTextEquivFromIDRefs(
while ((refContent = iter.NextElem())) {
if (!aTextEquiv.IsEmpty()) aTextEquiv += ' ';
if (refContent->IsHTMLElement(nsGkAtoms::slot)) printf("jtd idref slot\n");
// Note that we're in an aria-labelledby or aria-describedby traversal.
if (isAriaTraversal) {
sInAriaRelationTraversal = true;
}
// Reset the aria-labelledby / aria-describedby traversal tracking when we
// exit. Reset on scope exit because NS_ENSURE_SUCCESS may return.
auto onExit = MakeScopeExit([isAriaTraversal]() {
if (isAriaTraversal) {
sInAriaRelationTraversal = false;
}
});
nsresult rv =
AppendTextEquivFromContent(aAccessible, refContent, &aTextEquiv);
NS_ENSURE_SUCCESS(rv, rv);
@ -75,21 +124,36 @@ nsresult nsTextEquivUtils::AppendTextEquivFromContent(
const LocalAccessible* aInitiatorAcc, nsIContent* aContent,
nsAString* aString) {
// Prevent recursion which can cause infinite loops.
if (sInitiatorAcc) return NS_OK;
LocalAccessible* accessible =
aInitiatorAcc->Document()->GetAccessible(aContent);
if (sReferencedAccs.Contains(aInitiatorAcc) ||
sReferencedAccs.Contains(accessible)) {
return NS_OK;
}
sInitiatorAcc = aInitiatorAcc;
// Remember the initiating accessible so we know when we've returned to it.
if (sReferencedAccs.IsEmpty()) {
sInitiatorAcc = aInitiatorAcc;
}
sReferencedAccs.Insert(aInitiatorAcc);
nsresult rv = NS_ERROR_FAILURE;
if (LocalAccessible* accessible =
aInitiatorAcc->Document()->GetAccessible(aContent)) {
if (accessible) {
rv = AppendFromAccessible(accessible, aString);
sReferencedAccs.Insert(accessible);
} else {
// The given content is invisible or otherwise inaccessible, so use the DOM
// subtree.
rv = AppendFromDOMNode(aContent, aString);
}
sInitiatorAcc = nullptr;
// Once the text alternative computation is complete (i.e., once we've
// returned to the initiator acc), clear out the referenced accessibles and
// reset the initiator acc.
if (aInitiatorAcc == sInitiatorAcc) {
sReferencedAccs.Clear();
sInitiatorAcc = nullptr;
}
return rv;
}
@ -147,6 +211,10 @@ nsresult nsTextEquivUtils::AppendFromAccessibleChildren(
uint32_t childCount = aAccessible->ChildCount();
for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
Accessible* child = aAccessible->ChildAt(childIdx);
// If we've already consulted this child, don't consult it again.
if (sReferencedAccs.Contains(child)) {
continue;
}
rv = AppendFromAccessible(child, aString);
NS_ENSURE_SUCCESS(rv, rv);
}
@ -233,50 +301,31 @@ nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible,
return NS_OK_NO_NAME_CLAUSE_HANDLED;
}
// Implementation of step f. of text equivalent computation. If the given
// accessible is not root accessible (the accessible the text equivalent is
// computed for in the end) then append accessible value. Otherwise append
// value if and only if the given accessible is in the middle of its parent.
// Implementation of the "Embedded Control" step of the text alternative
// computation. If the given accessible is not the root accessible (the
// accessible the text alternative is computed for in the end) then append the
// accessible value.
if (aAccessible == sInitiatorAcc) {
return NS_OK_NO_NAME_CLAUSE_HANDLED;
}
// For listboxes in non-initiator computations, we need to get the selected
// item and append its text alternative.
nsAutoString text;
if (aAccessible != sInitiatorAcc) {
// For listboxes in non-initiator computations, we need to get the selected
// item and append its text alternative.
if (aAccessible->IsListControl()) {
Accessible* selected = aAccessible->GetSelectedItem(0);
if (selected) {
nsresult rv = AppendFromAccessible(selected, &text);
NS_ENSURE_SUCCESS(rv, rv);
return AppendString(aString, text) ? NS_OK
: NS_OK_NO_NAME_CLAUSE_HANDLED;
}
return NS_ERROR_FAILURE;
if (aAccessible->IsListControl()) {
Accessible* selected = aAccessible->GetSelectedItem(0);
if (selected) {
nsresult rv = AppendFromAccessible(selected, &text);
NS_ENSURE_SUCCESS(rv, rv);
return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;
}
aAccessible->Value(text);
return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;
return NS_ERROR_FAILURE;
}
// XXX: is it necessary to care the accessible is not a document?
if (aAccessible->IsDoc()) return NS_ERROR_UNEXPECTED;
aAccessible->Value(text);
for (Accessible* next = aAccessible->NextSibling(); next;
next = next->NextSibling()) {
if (!IsWhitespaceLeaf(next)) {
for (Accessible* prev = aAccessible->PrevSibling(); prev;
prev = prev->PrevSibling()) {
if (!IsWhitespaceLeaf(prev)) {
aAccessible->Value(text);
return AppendString(aString, text) ? NS_OK
: NS_OK_NO_NAME_CLAUSE_HANDLED;
}
}
}
}
return NS_OK_NO_NAME_CLAUSE_HANDLED;
return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;
}
nsresult nsTextEquivUtils::AppendFromDOMNode(nsIContent* aContent,

View file

@ -40,7 +40,7 @@ addAccessibleTask(``, async function (browser) {
testRole(dialog, ROLE_DIALOG);
let infoBody = focusedEl.DOMNode.ownerDocument.getElementById("infoBody");
testRelation(dialog, RELATION_DESCRIBED_BY, infoBody);
testDescr(dialog, "test ");
testDescr(dialog, "test ");
info("Dismissing alert");
let hidden = waitForEvent(EVENT_HIDE, frame);
EventUtils.synthesizeKey("KEY_Escape", {}, frame.DOMNode.contentWindow);

View file

@ -26,7 +26,7 @@ addAccessibleTask(
is(n1Label.getAttributeValue("AXTitle"), "Label");
let n2 = getNativeInterface(accDoc, "n2");
is(n2.getAttributeValue("AXDescription"), "TwoLabels");
is(n2.getAttributeValue("AXDescription"), "Two Labels");
let n3 = getNativeInterface(accDoc, "n3");
is(n3.getAttributeValue("AXDescription"), "ARIA Label");

View file

@ -102,10 +102,10 @@
// ////////////////////////////////////////////////////////////////////////
// label element
// The label element contains the button. The name is calculated from
// this button.
// Note: the name contains the content of the button.
testName("btn_label_inside", "text 10 text");
// The label element contains the button. The name of the button is
// calculated from the content of the label.
// Note: the name does not contain the content of the button.
testName("btn_label_inside", "texttext");
// The label element and the button are placed in the same form. Gets
// the name from the label subtree.
@ -120,7 +120,7 @@
testName("btn_label_indocument", "in document");
// Multiple label elements for single button
testName("btn_label_multi", "label1label2");
testName("btn_label_multi", "label1 label2");
// Multiple controls inside a label element
testName("ctrl_in_label_1", "Enable a button control");
@ -163,15 +163,15 @@
// ////////////////////////////////////////////////////////////////////////
// textarea name
// textarea's name should have the value, which initially is specified by
// a text child.
testName("textareawithchild", "Story Foo is ended.");
// textarea's name should not have the value, which initially is specified
// by a text child.
testName("textareawithchild", "Story is ended.");
// new textarea name should reflect the value change.
// new textarea name should not reflect the value change.
var elem = document.getElementById("textareawithchild");
elem.value = "Bar";
testName("textareawithchild", "Story Bar is ended.");
testName("textareawithchild", "Story is ended.");
// ////////////////////////////////////////////////////////////////////////
// controls having a value used as a part of computed name
@ -194,13 +194,13 @@
testName("textbox1", "days.");
testName("comboinmiddle", "Subscribe to ATOM feed.");
testName("combo4", "Subscribe to ATOM feed.");
testName("combo4", "Subscribe to feed.");
testName("comboinmiddle2", "Play the Haliluya sound when new mail arrives");
testName("combo5", null); // label isn't used as a name for control
testName("checkbox", "Play the Haliluya sound when new mail arrives");
testName("comboinmiddle3", "Play the Haliluya sound when new mail arrives");
testName("combo6", "Play the Haliluya sound when new mail arrives");
testName("combo6", "Play the sound when new mail arrives");
testName("comboinend", "This day was sunny");
testName("combo7", "This day was");

View file

@ -41,7 +41,7 @@
// Trick cases. Self and recursive referencing.
testName("rememberHistoryDays", "Remember 3 days");
testName("historyDays", "Remember 3 days");
testName("historyDays", "Remember days");
testName("rememberAfter", "days");
//////////////////////////////////////////////////////////////////////////

View file

@ -1,12 +1,6 @@
prefs: [layout.css.content.alt-text.enabled: true]
[comp_name_from_content.html]
[heading name from content for each child including nested link using aria-labelledby with nested image]
expected: FAIL
[heading name from content for each child including two nested links using aria-labelledby with nested image]
expected: FAIL
[heading with link referencing image using aria-labelledby, that in turn references text element via aria-labelledby]
expected: FAIL