commit f5110adff3b8369a59534eb7fd253aa8845750d2
parent c95ba551586aa388a2a98ecefc7d84f49f7fb172
Author: Eitan Isaacson <eitan@monotonous.org>
Date: Wed, 29 Oct 2025 00:15:03 +0000
Bug 1996134 - Exclude aria actions targets from name calculation. r=Jamie
There is a chance of extra name change events in the case where the
aria-actions attribute is modified in a way that does not alter the
subtree targets.
To be fair, this is true with aria-labeledby as well when spurious
IDREFs are inserted into the attribute's string.
Differential Revision: https://phabricator.services.mozilla.com/D270199
Diffstat:
4 files changed, 106 insertions(+), 1 deletion(-)
diff --git a/accessible/base/nsTextEquivUtils.cpp b/accessible/base/nsTextEquivUtils.cpp
@@ -59,6 +59,16 @@ nsresult nsTextEquivUtils::GetNameFromSubtree(
}
GetReferencedAccs().Insert(aAccessible);
+ if (nsIContent* content = aAccessible->GetContent()) {
+ AssociatedElementsIterator iter(aAccessible->Document(), content,
+ nsGkAtoms::aria_actions);
+ while (Accessible* actionTarget = iter.Next()) {
+ // aria-action targets are excluded from name calculation, so consider any
+ // of these targets as "referenced" for our purposes.
+ GetReferencedAccs().Insert(actionTarget);
+ }
+ }
+
if (GetRoleRule(aAccessible->Role()) == eNameFromSubtreeRule) {
// XXX: is it necessary to care the accessible is not a document?
if (aAccessible->IsContent()) {
diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp
@@ -823,7 +823,8 @@ static bool sIsAttrElementChanging = false;
void DocAccessible::AttributeWillChange(dom::Element* aElement,
int32_t aNameSpaceID,
- nsAtom* aAttribute, AttrModType) {
+ nsAtom* aAttribute,
+ AttrModType aModType) {
if (sIsAttrElementChanging) {
// See the comment above the definition of sIsAttrElementChanging.
return;
@@ -860,6 +861,19 @@ void DocAccessible::AttributeWillChange(dom::Element* aElement,
}
}
+ if ((aModType == AttrModType::Modification ||
+ aModType == AttrModType::Removal)) {
+ // If this is a modification or removal of aria-actions, and the
+ // accessible's name is calculated by the subtree, there may be a change to
+ // the name of the accessible.
+ // If this is a modification or removal of an id, an aria-actions relation
+ // might be severed, and thus change the name of any ancestors.
+ // XXX: We don't track the actual changes, so the name change event might
+ // be fired for not actual name change, but better to fire an event than to
+ // not.
+ MaybeHandleChangeToAriaActions(accessible, aAttribute);
+ }
+
// If attribute affects accessible's state, store the old state so we can
// later compare it against the state of the accessible after the attribute
// change.
@@ -957,6 +971,19 @@ void DocAccessible::AttributeChanged(dom::Element* aElement,
if (IsAdditionOrModification(aModType)) {
AddDependentIDsFor(accessible, aAttribute);
AddDependentElementsFor(accessible, aAttribute);
+
+ // If this is a modification or addition of aria-actions, and the
+ // accessible's name is calculated by the subtree, there may be a change to
+ // the name of the accessible.
+ // If this is a modification or addition of an id, an aria-actions relation
+ // might be restored, and thus change the name of any ancestors.
+ // XXX: We don't track the actual changes, so the name change event might
+ // be fired for not actual name change, but better to fire an event than to
+ // not.
+ // In the case of a modification we may have already queued a name
+ // change event in the `AttributeWillChange` stage, but we rely on
+ // EventQueue to quash any duplicates.
+ MaybeHandleChangeToAriaActions(accessible, aAttribute);
}
}
@@ -3150,6 +3177,34 @@ void DocAccessible::MaybeHandleChangeToHiddenNameOrDescription(
}
}
+void DocAccessible::MaybeHandleChangeToAriaActions(LocalAccessible* aAcc,
+ const nsAtom* aAttribute) {
+ if (aAttribute == nsGkAtoms::aria_actions &&
+ nsTextEquivUtils::HasNameRule(aAcc, eNameFromSubtreeIfReqRule)) {
+ // Search for action targets in subtree, and fire a name change event
+ // on aAcc if any are found.
+ AssociatedElementsIterator iter(mDoc, aAcc->Elm(), nsGkAtoms::aria_actions);
+ while (LocalAccessible* target = iter.Next()) {
+ if (aAcc->IsAncestorOf(target)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAcc);
+ break;
+ }
+ }
+ }
+
+ if (aAttribute == nsGkAtoms::id) {
+ RelatedAccIterator iter(mDoc, aAcc->Elm(), nsGkAtoms::aria_actions);
+ while (LocalAccessible* host = iter.Next()) {
+ // Search for any ancestor action hosts and fire a name change
+ // if any are found that calculate their name from the subtree.
+ if (host->IsAncestorOf(aAcc) &&
+ nsTextEquivUtils::HasNameRule(host, eNameFromSubtreeIfReqRule)) {
+ mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, host);
+ }
+ }
+ }
+}
+
void DocAccessible::AttrElementWillChange(dom::Element* aElement,
nsAtom* aAttr) {
MOZ_ASSERT(!sIsAttrElementChanging);
diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h
@@ -850,6 +850,9 @@ class DocAccessible : public HyperTextAccessible,
*/
void MaybeHandleChangeToHiddenNameOrDescription(nsIContent* aChild);
+ void MaybeHandleChangeToAriaActions(LocalAccessible* aAcc,
+ const nsAtom* aAttribute);
+
void MaybeFireEventsForChangedPopover(LocalAccessible* aAcc);
PresShell* mPresShell;
diff --git a/accessible/tests/browser/e10s/browser_caching_name.js b/accessible/tests/browser/e10s/browser_caching_name.js
@@ -552,3 +552,40 @@ addAccessibleTask(
},
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
);
+
+/**
+ * Test that the name does not include aria-actions targets of itself.
+ */
+addAccessibleTask(
+ `<div role="tablist">
+ <div role="tab" id="tab" aria-actions="close pin">Title
+ <button id="close">Close</button>
+ <button id="pin">Pin</button>
+ </div>
+ </div>`,
+ async function testAriaActionInSubtree(browser, docAcc) {
+ const tab = findAccessibleChildByID(docAcc, "tab");
+ testName(tab, "Title");
+
+ let nameChanged = waitForEvent(EVENT_NAME_CHANGE, tab);
+ invokeSetAttribute(browser, "tab", "aria-actions", "pin");
+ await nameChanged;
+ testName(tab, "Title Close");
+
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, tab);
+ invokeSetAttribute(browser, "pin", "id", "broken-pin");
+ await nameChanged;
+ testName(tab, "Title Close Pin");
+
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, tab);
+ invokeSetAttribute(browser, "tab", "aria-actions", "close pin");
+ await nameChanged;
+ testName(tab, "Title Pin");
+
+ nameChanged = waitForEvent(EVENT_NAME_CHANGE, tab);
+ invokeSetAttribute(browser, "broken-pin", "id", "pin");
+ await nameChanged;
+ testName(tab, "Title");
+ },
+ { chrome: true, topLevel: true }
+);