Repository: electron/electron
Pull request: fix: chrome://accessibility drift

#### Description of Change

Refs CL:6870052

<details><summary>Before</summary>
<p>

<img width="793" height="596" alt="Screenshot 2026-01-27 at 11 34 10 AM" src="https://github.com/user-attachments/assets/56c8bd51-0ae9-4c24-b645-f17a270087b2" />

</p>
</details> 

<details><summary>After</summary>
<p>

<img width="787" height="597" alt="Screenshot 2026-01-27 at 12 01 43 PM" src="https://github.com/user-attachments/assets/4e4fc48d-e014-45e9-89ff-e9339c75e02c" />

</p>
</details> 

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue in `chrome://accessibility`.

Changed files:
filename: shell/browser/ui/webui/accessibility_ui.cc
patch: @@ -49,6 +49,10 @@
 #include "ui/views/widget/widget.h"
 #include "ui/views/widget/widget_delegate.h"
 
+#if BUILDFLAG(IS_WIN)
+#include "ui/accessibility/platform/ax_platform_node_win.h"
+#endif
+
 namespace {
 
 constexpr std::string_view kTargetsDataFile = "targets-data.json";
@@ -151,6 +155,28 @@ bool ShouldHandleAccessibilityRequestCallback(const std::string& path) {
   return path == kTargetsDataFile;
 }
 
+// Sets boolean values in `data` for each bit in `new_ax_mode` that differs from
+// that in `last_ax_mode`. Returns `true` if `data` was modified.
+void SetProcessModeBools(ui::AXMode ax_mode, base::Value::Dict& data) {
+  data.Set(kNative, ax_mode.has_mode(ui::AXMode::kNativeAPIs));
+  data.Set(kWeb, ax_mode.has_mode(ui::AXMode::kWebContents));
+  data.Set(kText, ax_mode.has_mode(ui::AXMode::kInlineTextBoxes));
+  data.Set(kExtendedProperties,
+           ax_mode.has_mode(ui::AXMode::kExtendedProperties));
+  data.Set(kHTML, ax_mode.has_mode(ui::AXMode::kHTML));
+  data.Set(kScreenReader, ax_mode.has_mode(ui::AXMode::kScreenReader));
+}
+
+#if BUILDFLAG(IS_WIN)
+// Sets values in `data` for the platform node counts in `counts`.
+void SetNodeCounts(const ui::AXPlatformNodeWin::Counts& counts,
+                   base::Value::Dict& data) {
+  data.Set("dormantCount", base::NumberToString(counts.dormant_nodes));
+  data.Set("liveCount", base::NumberToString(counts.live_nodes));
+  data.Set("ghostCount", base::NumberToString(counts.ghost_nodes));
+}
+#endif
+
 void HandleAccessibilityRequestCallback(
     content::BrowserContext* current_context,
     ui::AXMode initial_process_mode,
@@ -294,6 +320,10 @@ void HandleAccessibilityRequestCallback(
   }
   data.Set(kBrowsersField, std::move(window_list));
 
+#if BUILDFLAG(IS_WIN)
+  SetNodeCounts(ui::AXPlatformNodeWin::GetCounts(), data);
+#endif
+
   std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
       base::WriteJson(data).value_or("")));
 }
@@ -381,8 +411,13 @@ ElectronAccessibilityUI::ElectronAccessibilityUI(content::WebUI* web_ui)
 
 ElectronAccessibilityUI::~ElectronAccessibilityUI() = default;
 
-ElectronAccessibilityUIMessageHandler::ElectronAccessibilityUIMessageHandler() =
-    default;
+ElectronAccessibilityUIMessageHandler::ElectronAccessibilityUIMessageHandler()
+    : update_display_timer_(
+          FROM_HERE,
+          base::Seconds(1),
+          base::BindRepeating(
+              &ElectronAccessibilityUIMessageHandler::OnUpdateDisplayTimer,
+              base::Unretained(this))) {}
 
 void ElectronAccessibilityUIMessageHandler::GetRequestTypeAndFilters(
     const base::Value::Dict& data,
@@ -472,6 +507,10 @@ void ElectronAccessibilityUIMessageHandler::RegisterMessages() {
       base::BindRepeating(
           &AccessibilityUIMessageHandler::RequestAccessibilityEvents,
           base::Unretained(this)));
+
+  auto* web_contents = web_ui()->GetWebContents();
+  Observe(web_contents);
+  OnVisibilityChanged(web_contents->GetVisibility());
 }
 
 // static
@@ -482,3 +521,45 @@ void ElectronAccessibilityUIMessageHandler::RegisterPrefs(
   registry->RegisterStringPref(prefs::kShownAccessibilityApiType,
                                std::string(default_api_type));
 }
+
+void ElectronAccessibilityUIMessageHandler::OnVisibilityChanged(
+    content::Visibility visibility) {
+  if (visibility == content::Visibility::HIDDEN) {
+    update_display_timer_.Stop();
+  } else {
+    update_display_timer_.Reset();
+  }
+}
+
+void ElectronAccessibilityUIMessageHandler::OnUpdateDisplayTimer() {
+  // Collect the current state.
+  base::Value::Dict data;
+
+  SetProcessModeBools(
+      content::BrowserAccessibilityState::GetInstance()->GetAccessibilityMode(),
+      data);
+
+#if BUILDFLAG(IS_WIN)
+  SetNodeCounts(ui::AXPlatformNodeWin::GetCounts(), data);
+#endif  // BUILDFLAG(IS_WIN)
+
+  // Compute the delta from the last transmission.
+  for (auto scan = data.begin(); scan != data.end();) {
+    const auto& [new_key, new_value] = *scan;
+    if (const auto* old_value = last_data_.Find(new_key);
+        !old_value || *old_value != new_value) {
+      // This is a new value; remember it for the future.
+      last_data_.Set(new_key, new_value.Clone());
+      ++scan;
+    } else {
+      // This is the same as the last value; forget about it.
+      scan = data.erase(scan);
+    }
+  }
+
+  // Transmit any new values to the UI.
+  if (!data.empty()) {
+    AllowJavascript();
+    FireWebUIListener("updateDisplay", data);
+  }
+}

filename: shell/browser/ui/webui/accessibility_ui.h
patch: @@ -40,6 +40,19 @@ class ElectronAccessibilityUIMessageHandler
                                 std::string& allow_empty,
                                 std::string& deny);
   void RequestNativeUITree(const base::Value::List& args);
+
+  // content::WebContentsObserver:
+  void OnVisibilityChanged(content::Visibility visibility) override;
+
+  // Updates the UI with new data. Called periodically to keep the UI up-to-date
+  // while it is visible.
+  void OnUpdateDisplayTimer();
+
+  // The last data for display sent to the UI by OnUpdateDisplayTimer.
+  base::Value::Dict last_data_;
+
+  // A timer that runs while the UI is visible to call OnUpdateDisplayTimer.
+  base::RepeatingTimer update_display_timer_;
 };
 
 #endif  // ELECTRON_SHELL_BROWSER_UI_WEBUI_ACCESSIBILITY_UI_H_