tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Maccessible/base/nsTextEquivUtils.cpp | 10++++++++++
Maccessible/generic/DocAccessible.cpp | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Maccessible/generic/DocAccessible.h | 3+++
Maccessible/tests/browser/e10s/browser_caching_name.js | 37+++++++++++++++++++++++++++++++++++++
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 } +);