tor-browser

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

commit e1fd97aa19fc94ebe1ac8a6aa7e5119651fad502
parent 78703903abbf1c0ebc4b833dee0d46f0b3d2524d
Author: Eitan Isaacson <eitan@monotonous.org>
Date:   Wed, 15 Oct 2025 17:15:11 +0000

Bug 1994029 - P2: Support aria-actions in ATK. r=Jamie

This means that all ATK objects will have an Action interface because
they can become actionable at any moment with aria-actions.

Differential Revision: https://phabricator.services.mozilla.com/D268469

Diffstat:
Maccessible/atk/AccessibleWrap.cpp | 13+++----------
Maccessible/atk/nsMaiInterfaceAction.cpp | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Maccessible/basetypes/Accessible.h | 18+++++++++---------
Maccessible/ipc/RemoteAccessible.h | 5++---
Maccessible/tests/browser/atk/a11y_setup.py | 11+++++++++++
Maccessible/tests/browser/atk/browser_action.js | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Maccessible/tests/browser/atk/browser_atspi_interfaces.js | 2++
7 files changed, 214 insertions(+), 32 deletions(-)

diff --git a/accessible/atk/AccessibleWrap.cpp b/accessible/atk/AccessibleWrap.cpp @@ -305,16 +305,9 @@ static uint16_t CreateMaiInterfaces(Accessible* aAccessible) { interfaces |= 1 << MAI_INTERFACE_SELECTION; } - if (aAccessible->IsRemote()) { - if (aAccessible->IsActionable()) { - interfaces |= 1 << MAI_INTERFACE_ACTION; - } - } else { - // XXX: Harmonize this with remote accessibles - if (aAccessible->ActionCount()) { - interfaces |= 1 << MAI_INTERFACE_ACTION; - } - } + // XXX: Always include the action interface because aria-actions + // can define actions mid-life. + interfaces |= 1 << MAI_INTERFACE_ACTION; return interfaces; } diff --git a/accessible/atk/nsMaiInterfaceAction.cpp b/accessible/atk/nsMaiInterfaceAction.cpp @@ -10,6 +10,7 @@ #include "nsMai.h" #include "mozilla/Likely.h" #include "nsAccessibilityService.h" +#include "Relation.h" #include "RemoteAccessible.h" #include "nsString.h" @@ -20,8 +21,29 @@ extern "C" { static gboolean doActionCB(AtkAction* aAction, gint aActionIndex) { AtkObject* atkObject = ATK_OBJECT(aAction); - if (Accessible* acc = GetInternalObj(atkObject)) { - return acc->DoAction(aActionIndex); + Accessible* acc = GetInternalObj(atkObject); + if (!acc) { + // If we don't have an Accessible, we can't have any actions. + return false; + } + + if (aActionIndex < acc->ActionCount()) { + acc->DoAction(aActionIndex); + return true; + } + + // Check for custom actions. + Relation customActions(acc->RelationByType(RelationType::ACTION)); + gint actionIndex = acc->ActionCount(); + while (Accessible* target = customActions.Next()) { + if (target->HasPrimaryAction()) { + MOZ_ASSERT(target->ActionCount() > 0); + if (actionIndex == aActionIndex) { + target->DoAction(0); + return true; + } + actionIndex++; + } } return false; @@ -29,19 +51,52 @@ static gboolean doActionCB(AtkAction* aAction, gint aActionIndex) { static gint getActionCountCB(AtkAction* aAction) { AtkObject* atkObject = ATK_OBJECT(aAction); - if (Accessible* acc = GetInternalObj(atkObject)) { - return acc->ActionCount(); + Accessible* acc = GetInternalObj(atkObject); + if (!acc) { + // If we don't have an Accessible, we can't have any actions. + return 0; + } + + gint actionCount = acc->ActionCount(); + Relation customActions(acc->RelationByType(RelationType::ACTION)); + while (Accessible* target = customActions.Next()) { + if (target->HasPrimaryAction()) { + actionCount++; + } } - return 0; + return actionCount; } static const gchar* getActionDescriptionCB(AtkAction* aAction, gint aActionIndex) { AtkObject* atkObject = ATK_OBJECT(aAction); nsAutoString description; - if (Accessible* acc = GetInternalObj(atkObject)) { + Accessible* acc = GetInternalObj(atkObject); + if (!acc) { + // If we don't have an Accessible, we can't have any actions. + return 0; + } + + if (aActionIndex < acc->ActionCount()) { acc->ActionDescriptionAt(aActionIndex, description); + } else { + // Check for custom actions. + Relation customActions(acc->RelationByType(RelationType::ACTION)); + gint actionIndex = acc->ActionCount(); + while (Accessible* target = customActions.Next()) { + if (target->HasPrimaryAction()) { + MOZ_ASSERT(target->ActionCount() > 0); + if (actionIndex == aActionIndex) { + target->ActionDescriptionAt(0, description); + break; + } + actionIndex++; + } + } + } + + if (!description.IsEmpty()) { return AccessibleWrap::ReturnString(description); } @@ -50,10 +105,71 @@ static const gchar* getActionDescriptionCB(AtkAction* aAction, static const gchar* getActionNameCB(AtkAction* aAction, gint aActionIndex) { AtkObject* atkObject = ATK_OBJECT(aAction); - nsAutoString autoStr; - if (Accessible* acc = GetInternalObj(atkObject)) { - acc->ActionNameAt(aActionIndex, autoStr); - return AccessibleWrap::ReturnString(autoStr); + nsAutoString name; + Accessible* acc = GetInternalObj(atkObject); + if (!acc) { + // If we don't have an Accessible, we can't have any actions. + return 0; + } + + if (aActionIndex < acc->ActionCount()) { + acc->ActionNameAt(aActionIndex, name); + } else { + // Check for custom actions. + Relation customActions(acc->RelationByType(RelationType::ACTION)); + gint actionIndex = acc->ActionCount(); + while (Accessible* target = customActions.Next()) { + if (target->HasPrimaryAction()) { + MOZ_ASSERT(target->ActionCount() > 0); + if (actionIndex == aActionIndex) { + name.AssignLiteral("custom"); + nsAutoString domNodeId; + target->DOMNodeID(domNodeId); + if (!domNodeId.IsEmpty()) { + name.AppendPrintf("_%s", NS_ConvertUTF16toUTF8(domNodeId).get()); + } + break; + } + actionIndex++; + } + } + } + + if (!name.IsEmpty()) { + return AccessibleWrap::ReturnString(name); + } + + return nullptr; +} + +static const gchar* getActionLocalizedNameCB(AtkAction* aAction, + gint aActionIndex) { + AtkObject* atkObject = ATK_OBJECT(aAction); + nsAutoString name; + Accessible* acc = GetInternalObj(atkObject); + if (!acc) { + // If we don't have an Accessible, we can't have any actions. + return 0; + } + + if (aActionIndex >= acc->ActionCount()) { + // Check for custom actions. + Relation customActions(acc->RelationByType(RelationType::ACTION)); + gint actionIndex = acc->ActionCount(); + while (Accessible* target = customActions.Next()) { + if (target->HasPrimaryAction()) { + MOZ_ASSERT(target->ActionCount() > 0); + if (actionIndex == aActionIndex) { + target->Name(name); + break; + } + actionIndex++; + } + } + } + + if (!name.IsEmpty()) { + return AccessibleWrap::ReturnString(name); } return nullptr; @@ -80,4 +196,5 @@ void actionInterfaceInitCB(AtkActionIface* aIface) { aIface->get_description = getActionDescriptionCB; aIface->get_keybinding = getKeyBindingCB; aIface->get_name = getActionNameCB; + aIface->get_localized_name = getActionLocalizedNameCB; } diff --git a/accessible/basetypes/Accessible.h b/accessible/basetypes/Accessible.h @@ -780,6 +780,15 @@ class Accessible { */ virtual int32_t GetLevel(bool aFast) const; + /** + * Return true if accessible has a primary action directly related to it, like + * "click", "activate", "press", "jump", "open", "close", etc. A non-primary + * action would be a complementary one like "showlongdesc". + * If an accessible has an action that is associated with an ancestor, it is + * not a primary action either. + */ + virtual bool HasPrimaryAction() const = 0; + protected: // Some abstracted group utility methods. @@ -814,15 +823,6 @@ class Accessible { const Accessible* ActionAncestor() const; /** - * Return true if accessible has a primary action directly related to it, like - * "click", "activate", "press", "jump", "open", "close", etc. A non-primary - * action would be a complementary one like "showlongdesc". - * If an accessible has an action that is associated with an ancestor, it is - * not a primary action either. - */ - virtual bool HasPrimaryAction() const = 0; - - /** * Apply states which are implied by other information common to both * LocalAccessible and RemoteAccessible. */ diff --git a/accessible/ipc/RemoteAccessible.h b/accessible/ipc/RemoteAccessible.h @@ -390,6 +390,8 @@ class RemoteAccessible : public Accessible, public HyperTextAccessibleBase { virtual bool IsPopover() const override; + virtual bool HasPrimaryAction() const override; + #if !defined(XP_WIN) void Announce(const nsString& aAnnouncement, uint16_t aPriority); #endif // !defined(XP_WIN) @@ -492,9 +494,6 @@ class RemoteAccessible : public Accessible, public HyperTextAccessibleBase { virtual void GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) override; - - virtual bool HasPrimaryAction() const override; - nsAtom* GetPrimaryAction() const; virtual nsTArray<int32_t>& GetCachedHyperTextOffsets() override; diff --git a/accessible/tests/browser/atk/a11y_setup.py b/accessible/tests/browser/atk/a11y_setup.py @@ -21,11 +21,22 @@ pyatspiFile = subprocess.check_output( ), encoding="utf-8", ).rstrip() +giFile = subprocess.check_output( + ( + os.path.join(sys.base_prefix, "bin", "python3"), + "-c", + "import gi; print(gi.__file__)", + ), + encoding="utf-8", +).rstrip() sys.path.append(os.path.dirname(os.path.dirname(pyatspiFile))) +sys.path.append(os.path.dirname(os.path.dirname(giFile))) import pyatspi sys.path.pop() +sys.path.pop() del pyatspiFile +del giFile def setup(): diff --git a/accessible/tests/browser/atk/browser_action.js b/accessible/tests/browser/atk/browser_action.js @@ -28,3 +28,63 @@ addAccessibleTask( await nameChanged; } ); + +/** + * Test aria-actions. + */ +addAccessibleTask( + ` + <div id="container"> + <dialog aria-actions="btn1" id="dlg1" open> + Hello + <form method="dialog"> + <button id="btn1">Close</button> + </form> + </dialog> + <dialog aria-actions="btn2" id="dlg2" onclick="" open> + Dialog with its own click listener + <form method="dialog"> + <button id="btn2">Close</button> + </form> + </dialog> + </div>`, + async function () { + let actions = await runPython(` + global doc + doc = getDoc() + global dlg1 + dlg1 = findByDomId(doc, "dlg1").queryAction() + return str([[dlg1.getName(i), dlg1.getLocalizedName(i), dlg1.getDescription(i)] for i in range(dlg1.get_nActions())]) + `); + is( + actions, + "[['custom_btn1', 'Close', 'Press']]", + "dlg1 has correct actions" + ); + + let reorder = waitForEvent(EVENT_REORDER, "container"); + await runPython(` + dlg1.doAction(0) + `); + await reorder; + + // Test dialog with its own click listener, and therefore has the aria-actions + // target actions appended to its own actions. + actions = await runPython(` + global dlg2 + dlg2 = findByDomId(doc, "dlg2").queryAction() + return str([[dlg2.getName(i), dlg2.getLocalizedName(i), dlg2.getDescription(i)] for i in range(dlg2.get_nActions())]) + `); + is( + actions, + "[['click', '', 'Click'], ['custom_btn2', 'Close', 'Press']]", + "dlg2 has correct actions" + ); + + reorder = waitForEvent(EVENT_REORDER, "container"); + await runPython(` + dlg2.doAction(1) + `); + await reorder; + } +); diff --git a/accessible/tests/browser/atk/browser_atspi_interfaces.js b/accessible/tests/browser/atk/browser_atspi_interfaces.js @@ -31,6 +31,7 @@ addAccessibleTask( await checkInterfaces("p", [ "Accessible", + "Action", "Collection", "Component", "EditableText", @@ -50,6 +51,7 @@ addAccessibleTask( ]); await checkInterfaces("range_input", [ "Accessible", + "Action", "Collection", "Component", "Hyperlink",