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:
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",