Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / client / accessibility / panel.js
blobbf23cbd6bfd05586fe7922b085ddfb254d7894c8
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
8 loader.lazyRequireGetter(
9 this,
10 "AccessibilityProxy",
11 "resource://devtools/client/accessibility/accessibility-proxy.js",
12 true
14 loader.lazyRequireGetter(
15 this,
16 "Picker",
17 "resource://devtools/client/accessibility/picker.js",
18 true
20 const {
21 A11Y_SERVICE_DURATION,
22 } = require("resource://devtools/client/accessibility/constants.js");
24 // The panel's window global is an EventEmitter firing the following events:
25 const EVENTS = {
26 // When the accessibility inspector has a new accessible front selected.
27 NEW_ACCESSIBLE_FRONT_SELECTED: "Accessibility:NewAccessibleFrontSelected",
28 // When the accessibility inspector has a new accessible front highlighted.
29 NEW_ACCESSIBLE_FRONT_HIGHLIGHTED:
30 "Accessibility:NewAccessibleFrontHighlighted",
31 // When the accessibility inspector has a new accessible front inspected.
32 NEW_ACCESSIBLE_FRONT_INSPECTED: "Accessibility:NewAccessibleFrontInspected",
33 // When the accessibility inspector is updated.
34 ACCESSIBILITY_INSPECTOR_UPDATED:
35 "Accessibility:AccessibilityInspectorUpdated",
36 // When accessibility panel UI is initialized (rendered).
37 INITIALIZED: "Accessibility:Initialized",
38 // When accessibile object properties are updated in the panel sidebar for a
39 // new accessible object.
40 PROPERTIES_UPDATED: "Accessibility:PropertiesUpdated",
43 /**
44 * This object represents Accessibility panel. It's responsibility is to
45 * render Accessibility Tree of the current debugger target and the sidebar that
46 * displays current relevant accessible details.
48 function AccessibilityPanel(iframeWindow, toolbox, commands) {
49 this.panelWin = iframeWindow;
50 this._toolbox = toolbox;
51 this._commands = commands;
53 this.onPanelVisibilityChange = this.onPanelVisibilityChange.bind(this);
54 this.onNewAccessibleFrontSelected =
55 this.onNewAccessibleFrontSelected.bind(this);
56 this.onAccessibilityInspectorUpdated =
57 this.onAccessibilityInspectorUpdated.bind(this);
58 this.updateA11YServiceDurationTimer =
59 this.updateA11YServiceDurationTimer.bind(this);
60 this.forceUpdatePickerButton = this.forceUpdatePickerButton.bind(this);
61 this.onLifecycleEvent = this.onLifecycleEvent.bind(this);
63 EventEmitter.decorate(this);
66 AccessibilityPanel.prototype = {
67 /**
68 * Open is effectively an asynchronous constructor.
70 async open() {
71 if (this._opening) {
72 await this._opening;
73 return this._opening;
76 // This first promise includes initialization of proxy *and* the call to forceRefresh
77 let resolver;
78 this._opening = new Promise(resolve => {
79 resolver = resolve;
80 });
82 // This second promise only include the initialization of proxy and few other things,
83 // but not the call to forceRefresh.
84 const { promise, resolve } = Promise.withResolvers();
85 this.initializedPromise = promise;
87 this._telemetry = this._toolbox.telemetry;
88 this.panelWin.gTelemetry = this._telemetry;
90 this._toolbox.on("select", this.onPanelVisibilityChange);
92 this.panelWin.EVENTS = EVENTS;
93 EventEmitter.decorate(this.panelWin);
94 this.panelWin.on(
95 EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED,
96 this.onNewAccessibleFrontSelected
98 this.panelWin.on(
99 EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED,
100 this.onAccessibilityInspectorUpdated
103 this.picker = new Picker(this);
104 this.fluentBundles = await this.createFluentBundles();
106 this.accessibilityProxy = new AccessibilityProxy(this._commands, this);
108 await this.accessibilityProxy.initialize();
110 this.accessibilityProxy.startListeningForLifecycleEvents({
111 init: this.onLifecycleEvent,
112 shutdown: this.onLifecycleEvent,
115 // Start recording the duration where a11y service is enabled via the proxy.
116 this.updateA11YServiceDurationTimer();
118 // Resolve the `this.initializedPromise`
119 resolve();
121 // Force rendering the panel once everything is initialized
122 await this.forceRefresh();
124 resolver(this);
125 return this._opening;
129 * Retrieve message contexts for the current locales, and return them as an
130 * array of FluentBundles elements.
132 async createFluentBundles() {
133 const locales = Services.locale.appLocalesAsBCP47;
134 const generator = L10nRegistry.getInstance().generateBundles(locales, [
135 "devtools/client/accessibility.ftl",
138 // Return value of generateBundles is a generator and should be converted to
139 // a sync iterable before using it with React.
140 const contexts = [];
141 for await (const message of generator) {
142 contexts.push(message);
145 return contexts;
148 onLifecycleEvent() {
149 this.updateA11YServiceDurationTimer();
150 this.forceUpdatePickerButton();
153 onNewAccessibleFrontSelected(selected) {
154 this.emit("new-accessible-front-selected", selected);
157 onAccessibilityInspectorUpdated() {
158 this.emit("accessibility-inspector-updated");
162 * Make sure the panel is refreshed when the page is reloaded. The panel is
163 * refreshed immediatelly if it's currently selected or lazily when the user
164 * actually selects it.
166 async forceRefresh() {
167 this.shouldRefresh = true;
169 // Wait for initialization to be done, in case this is called early on.
170 await this.initializedPromise;
171 const onUpdated = this.panelWin.once(EVENTS.INITIALIZED);
172 this.refresh();
173 await onUpdated;
175 this.emit("reloaded");
179 * Make sure the panel is refreshed (if needed) when it's selected.
181 onPanelVisibilityChange() {
182 this._opening.then(() => this.refresh());
185 refresh() {
186 this.cancelPicker();
188 if (!this.isVisible) {
189 // Do not refresh if the panel isn't visible.
190 return;
193 // Do not refresh if it isn't necessary.
194 if (!this.shouldRefresh) {
195 return;
197 // Alright reset the flag we are about to refresh the panel.
198 this.shouldRefresh = false;
199 const {
200 supports,
201 getAccessibilityTreeRoot,
202 startListeningForAccessibilityEvents,
203 stopListeningForAccessibilityEvents,
204 audit,
205 simulate,
206 toggleDisplayTabbingOrder,
207 enableAccessibility,
208 resetAccessiblity,
209 startListeningForLifecycleEvents,
210 stopListeningForLifecycleEvents,
211 startListeningForParentLifecycleEvents,
212 stopListeningForParentLifecycleEvents,
213 highlightAccessible,
214 unhighlightAccessible,
215 } = this.accessibilityProxy;
216 this.postContentMessage("initialize", {
217 fluentBundles: this.fluentBundles,
218 toolbox: this._toolbox,
219 supports,
220 getAccessibilityTreeRoot,
221 startListeningForAccessibilityEvents,
222 stopListeningForAccessibilityEvents,
223 audit,
224 simulate,
225 toggleDisplayTabbingOrder,
226 enableAccessibility,
227 resetAccessiblity,
228 startListeningForLifecycleEvents,
229 stopListeningForLifecycleEvents,
230 startListeningForParentLifecycleEvents,
231 stopListeningForParentLifecycleEvents,
232 highlightAccessible,
233 unhighlightAccessible,
237 updateA11YServiceDurationTimer() {
238 if (this.accessibilityProxy.enabled) {
239 this._telemetry.start(A11Y_SERVICE_DURATION, this);
240 } else {
241 this._telemetry.finish(A11Y_SERVICE_DURATION, this, true);
245 selectAccessible(accessibleFront) {
246 this.postContentMessage("selectAccessible", accessibleFront);
249 selectAccessibleForNode(nodeFront, reason) {
250 if (reason) {
251 Glean.devtoolsAccessibility.selectAccessibleForNode[reason].add(1);
254 this.postContentMessage("selectNodeAccessible", nodeFront);
257 highlightAccessible(accessibleFront) {
258 this.postContentMessage("highlightAccessible", accessibleFront);
261 postContentMessage(type, ...args) {
262 const event = new this.panelWin.MessageEvent("devtools/chrome/message", {
263 bubbles: true,
264 cancelable: true,
265 data: { type, args },
268 this.panelWin.dispatchEvent(event);
271 updatePickerButton() {
272 this.picker && this.picker.updateButton();
275 forceUpdatePickerButton() {
276 // Only update picker button when the panel is selected.
277 if (!this.isVisible) {
278 return;
281 this.updatePickerButton();
282 // Calling setToolboxButtons to make sure toolbar is forced to re-render.
283 this._toolbox.component.setToolboxButtons(this._toolbox.toolbarButtons);
286 togglePicker() {
287 this.picker && this.picker.toggle();
290 cancelPicker() {
291 this.picker && this.picker.cancel();
294 stopPicker() {
295 this.picker && this.picker.stop();
299 * Return true if the Accessibility panel is currently selected.
301 get isVisible() {
302 return this._toolbox.currentToolId === "accessibility";
305 destroy() {
306 if (this._destroyed) {
307 return;
309 this._destroyed = true;
311 this.postContentMessage("destroy");
313 if (this.accessibilityProxy) {
314 this.accessibilityProxy.stopListeningForLifecycleEvents({
315 init: this.onLifecycleEvent,
316 shutdown: this.onLifecycleEvent,
318 this.accessibilityProxy.destroy();
319 this.accessibilityProxy = null;
320 this.initializedPromise = null;
323 this._toolbox.off("select", this.onPanelVisibilityChange);
325 this.panelWin.off(
326 EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED,
327 this.onNewAccessibleFrontSelected
329 this.panelWin.off(
330 EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED,
331 this.onAccessibilityInspectorUpdated
334 // Older versions of devtools server do not support picker functionality.
335 if (this.picker) {
336 this.picker.release();
337 this.picker = null;
340 this._telemetry = null;
341 this.panelWin.gTelemetry = null;
343 this.emit("destroyed");
347 // Exports from this module
348 exports.AccessibilityPanel = AccessibilityPanel;