fix: [a11y] fire AXMenuOpened event when ARIA menu is added to DOM (#50506)

* fix: fire AXMenuOpened event when a visible ARIA menu instance is added to the DOM

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* fix: remove redundent FireMenuPopupEndForDeletedMenus

MENU_POPUP_END for deleted menus is already handled by
AXTreeManager::OnNodeWillBeDeleted, which
fires the event directly on the menu node before destruction.

Co-authored-by: Keeley Hammond <khammond@slack-corp.com>

* chore: add feature flag (kDynamicMenuPopupEvents)

Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>

* chore: update patches

Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>

* chore: update patches after trop

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <khammond@slack-corp.com>
Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>
Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
This commit is contained in:
trop[bot]
2026-03-26 09:54:27 -07:00
committed by GitHub
parent 880b1e08e7
commit cbae32aac6
2 changed files with 96 additions and 0 deletions

View File

@@ -147,3 +147,4 @@ fix_update_dbus_signal_signature_for_xdg_globalshortcuts_portal.patch
fix_set_correct_app_id_on_linux.patch
fix_pass_trigger_for_global_shortcuts_on_wayland.patch
feat_plumb_node_integration_in_worker_through_workersettings.patch
fix_fire_menu_popup_start_for_dynamically_created_aria_menus.patch

View File

@@ -0,0 +1,95 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Keeley Hammond <khammond@slack-corp.com>
Date: Thu, 19 Mar 2026 00:34:37 -0700
Subject: fix: fire MENU_POPUP_START for dynamically created ARIA menus
When an ARIA menu element is dynamically created (e.g. via appendChild)
rather than being shown by toggling visibility, the AXMenuOpened event
was not fired. The OnIgnoredChanged path handles the visibility toggle
case, but OnAtomicUpdateFinished did not fire MENU_POPUP_START for
newly created menu nodes.
Previous attempts to fix this (crbug.com/1254875) were reverted because
they fired the event too eagerly in OnNodeCreated (before the tree was
fully formed) and without filtering, causing regressions with screen
readers on pages that misused role="menu".
This fix addresses both issues:
1. Fires MENU_POPUP_START in OnAtomicUpdateFinished (after the tree
update is complete) rather than in OnNodeCreated.
2. Only fires if the menu has at least one menuitem child, filtering
out false positives from misused role="menu" elements.
MENU_POPUP_END for deleted menus is already handled by
AXTreeManager::OnNodeWillBeDeleted, which fires the event directly
on the menu node before destruction.
The change is behind the DynamicMenuPopupEvents feature flag, disabled
by default, to allow stabilization before enabling by default. Enable
with --enable-features=DynamicMenuPopupEvents.
This patch can be removed when a CL containing the fix is accepted
into Chromium.
Bug: 40794596
diff --git a/ui/accessibility/ax_event_generator.cc b/ui/accessibility/ax_event_generator.cc
index 5e0d7a48b4a039db67b5cc6b7e86103739702b40..517fb5e9904f3907de177e172c76328910bb7333 100644
--- a/ui/accessibility/ax_event_generator.cc
+++ b/ui/accessibility/ax_event_generator.cc
@@ -4,6 +4,7 @@
#include "ui/accessibility/ax_event_generator.h"
+#include "base/feature_list.h"
#include "base/no_destructor.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_event.h"
@@ -12,6 +13,12 @@
namespace ui {
+// Feature flag for firing MENU_POPUP_START for dynamically created ARIA menus.
+// Disabled by default to allow stabilization before enabling globally.
+BASE_FEATURE(kDynamicMenuPopupEvents,
+ "DynamicMenuPopupEvents",
+ base::FEATURE_DISABLED_BY_DEFAULT);
+
namespace {
bool HasEvent(const std::set<AXEventGenerator::EventParams>& node_events,
@@ -914,12 +921,31 @@ void AXEventGenerator::OnAtomicUpdateFinished(
/*new_value*/ true);
}
- if (IsAlert(change.node->GetRole()))
+ if (IsAlert(change.node->GetRole())) {
AddEvent(change.node, Event::ALERT);
- else if (change.node->data().IsActiveLiveRegionRoot())
+ } else if (change.node->data().IsActiveLiveRegionRoot()) {
AddEvent(change.node, Event::LIVE_REGION_CREATED);
- else if (change.node->data().IsContainedInActiveLiveRegion())
+ } else if (change.node->data().IsContainedInActiveLiveRegion()) {
FireLiveRegionEvents(change.node, /* is_removal */ false);
+ }
+
+ // Fire MENU_POPUP_START when a menu is dynamically created (e.g. via
+ // appendChild). The OnIgnoredChanged path handles menus that already exist
+ // in the DOM and are shown/hidden. This handles the case where the menu
+ // element itself is created on the fly.
+ // Only fire if the menu has at least one menuitem child, to avoid false
+ // positives from elements that misuse role="menu".
+ if (base::FeatureList::IsEnabled(kDynamicMenuPopupEvents) &&
+ change.node->GetRole() == ax::mojom::Role::kMenu &&
+ !change.node->IsInvisibleOrIgnored()) {
+ for (auto iter = change.node->UnignoredChildrenBegin();
+ iter != change.node->UnignoredChildrenEnd(); ++iter) {
+ if (IsMenuItem(iter->GetRole())) {
+ AddEvent(change.node, Event::MENU_POPUP_START);
+ break;
+ }
+ }
+ }
}
FireActiveDescendantEvents();