commit fd81a764be34cd0a4b9720f33104e9c24eafda97
parent 8641808763bed9ae74a03772f1b21f8c654ddbe8
Author: Eitan Isaacson <eitan@monotonous.org>
Date: Wed, 22 Oct 2025 03:44:58 +0000
Bug 1994031 - P2: Support aria-actions IA2. r=Jamie
Differential Revision: https://phabricator.services.mozilla.com/D268736
Diffstat:
2 files changed, 133 insertions(+), 3 deletions(-)
diff --git a/accessible/tests/browser/windows/ia2/browser_action.js b/accessible/tests/browser/windows/ia2/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 testAriaActions() {
+ let actions = await runPython(`
+ global doc
+ doc = getDocIa2()
+ global dlg1
+ dlg1 = findIa2ByDomId(doc, "dlg1").QueryInterface(IAccessibleAction)
+ return str([[dlg1.name(i), dlg1.localizedName(i), dlg1.description(i)] for i in range(dlg1.nActions())])
+ `);
+ is(
+ actions,
+ "[['custom_btn1', 'Close', 'Close']]",
+ "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 = findIa2ByDomId(doc, "dlg2").QueryInterface(IAccessibleAction)
+ return str([[dlg2.name(i), dlg2.localizedName(i), dlg2.description(i)] for i in range(dlg2.nActions())])
+ `);
+ is(
+ actions,
+ "[['click', 'Click', 'Click'], ['custom_btn2', 'Close', 'Close']]",
+ "dlg2 has correct actions"
+ );
+
+ reorder = waitForEvent(EVENT_REORDER, "container");
+ await runPython(`
+ dlg2.doAction(1)
+ `);
+ await reorder;
+ }
+);
diff --git a/accessible/windows/ia2/ia2AccessibleAction.cpp b/accessible/windows/ia2/ia2AccessibleAction.cpp
@@ -12,6 +12,7 @@
#include "AccessibleWrap.h"
#include "IUnknownImpl.h"
#include "MsaaAccessible.h"
+#include "Relation.h"
using namespace mozilla::a11y;
@@ -48,6 +49,13 @@ ia2AccessibleAction::nActions(long* aActionCount) {
if (!acc) return CO_E_OBJNOTCONNECTED;
*aActionCount = acc->ActionCount();
+ Relation customActions(acc->RelationByType(RelationType::ACTION));
+ while (Accessible* target = customActions.Next()) {
+ if (target->HasPrimaryAction()) {
+ (*aActionCount)++;
+ }
+ }
+
return S_OK;
}
@@ -57,7 +65,29 @@ ia2AccessibleAction::doAction(long aActionIndex) {
if (!acc) return CO_E_OBJNOTCONNECTED;
uint8_t index = static_cast<uint8_t>(aActionIndex);
- return acc->DoAction(index) ? S_OK : E_INVALIDARG;
+
+ if (index < acc->ActionCount()) {
+ DebugOnly<bool> success = acc->DoAction(aActionIndex);
+ MOZ_ASSERT(success, "Failed to perform action");
+ return S_OK;
+ }
+
+ // Check for custom actions.
+ Relation customActions(acc->RelationByType(RelationType::ACTION));
+ uint8_t actionIndex = acc->ActionCount();
+ while (Accessible* target = customActions.Next()) {
+ if (target->HasPrimaryAction()) {
+ MOZ_ASSERT(target->ActionCount() > 0);
+ if (actionIndex == index) {
+ DebugOnly<bool> success = target->DoAction(0);
+ MOZ_ASSERT(success, "Failed to perform action");
+ return S_OK;
+ }
+ actionIndex++;
+ }
+ }
+
+ return E_INVALIDARG;
}
STDMETHODIMP
@@ -124,7 +154,29 @@ ia2AccessibleAction::get_name(long aActionIndex, BSTR* aName) {
nsAutoString name;
uint8_t index = static_cast<uint8_t>(aActionIndex);
- acc->ActionNameAt(index, name);
+ if (index < acc->ActionCount()) {
+ acc->ActionNameAt(aActionIndex, name);
+ } else {
+ // Check for custom actions.
+ Relation customActions(acc->RelationByType(RelationType::ACTION));
+ uint8_t actionIndex = acc->ActionCount();
+ while (Accessible* target = customActions.Next()) {
+ if (target->HasPrimaryAction()) {
+ MOZ_ASSERT(target->ActionCount() > 0);
+ if (actionIndex == index) {
+ name.AssignLiteral("custom");
+ nsAutoString domNodeId;
+ target->DOMNodeID(domNodeId);
+ if (!domNodeId.IsEmpty()) {
+ name.AppendPrintf("_%s", NS_ConvertUTF16toUTF8(domNodeId).get());
+ }
+ break;
+ }
+ actionIndex++;
+ }
+ }
+ }
+
if (name.IsEmpty()) return E_INVALIDARG;
*aName = ::SysAllocStringLen(name.get(), name.Length());
@@ -142,7 +194,25 @@ ia2AccessibleAction::get_localizedName(long aActionIndex,
nsAutoString description;
uint8_t index = static_cast<uint8_t>(aActionIndex);
- acc->ActionDescriptionAt(index, description);
+
+ if (aActionIndex < acc->ActionCount()) {
+ acc->ActionDescriptionAt(index, description);
+ } else {
+ // Check for custom actions.
+ Relation customActions(acc->RelationByType(RelationType::ACTION));
+ uint8_t actionIndex = acc->ActionCount();
+ while (Accessible* target = customActions.Next()) {
+ if (target->HasPrimaryAction()) {
+ MOZ_ASSERT(target->ActionCount() > 0);
+ if (actionIndex == index) {
+ target->Name(description);
+ break;
+ }
+ actionIndex++;
+ }
+ }
+ }
+
if (description.IsEmpty()) return S_FALSE;
*aLocalizedName =