Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / client / accessibility / accessibility-proxy.js
blobe7019a73319982ea4c42c60dfe8aa38410dab2ed
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/. */
5 "use strict";
7 loader.lazyRequireGetter(
8 this,
9 "CombinedProgress",
10 "resource://devtools/client/accessibility/utils/audit.js",
11 true
14 const {
15 accessibility: { AUDIT_TYPE },
16 } = require("resource://devtools/shared/constants.js");
17 const {
18 FILTERS,
19 } = require("resource://devtools/client/accessibility/constants.js");
21 /**
22 * Component responsible for tracking all Accessibility fronts in parent and
23 * content processes.
25 class AccessibilityProxy {
26 #panel;
27 #initialized;
28 constructor(commands, panel) {
29 this.commands = commands;
30 this.#panel = panel;
32 this.#initialized = false;
33 this._accessibilityWalkerFronts = new Set();
34 this.lifecycleEvents = new Map();
35 this.accessibilityEvents = new Map();
37 this.audit = this.audit.bind(this);
38 this.enableAccessibility = this.enableAccessibility.bind(this);
39 this.getAccessibilityTreeRoot = this.getAccessibilityTreeRoot.bind(this);
40 this.resetAccessiblity = this.resetAccessiblity.bind(this);
41 this.startListeningForAccessibilityEvents =
42 this.startListeningForAccessibilityEvents.bind(this);
43 this.startListeningForLifecycleEvents =
44 this.startListeningForLifecycleEvents.bind(this);
45 this.startListeningForParentLifecycleEvents =
46 this.startListeningForParentLifecycleEvents.bind(this);
47 this.stopListeningForAccessibilityEvents =
48 this.stopListeningForAccessibilityEvents.bind(this);
49 this.stopListeningForLifecycleEvents =
50 this.stopListeningForLifecycleEvents.bind(this);
51 this.stopListeningForParentLifecycleEvents =
52 this.stopListeningForParentLifecycleEvents.bind(this);
53 this.highlightAccessible = this.highlightAccessible.bind(this);
54 this.unhighlightAccessible = this.unhighlightAccessible.bind(this);
55 this.onTargetAvailable = this.onTargetAvailable.bind(this);
56 this.onTargetDestroyed = this.onTargetDestroyed.bind(this);
57 this.onTargetSelected = this.onTargetSelected.bind(this);
58 this.onAccessibilityFrontAvailable =
59 this.onAccessibilityFrontAvailable.bind(this);
60 this.onAccessibilityFrontDestroyed =
61 this.onAccessibilityFrontDestroyed.bind(this);
62 this.onAccessibleWalkerFrontAvailable =
63 this.onAccessibleWalkerFrontAvailable.bind(this);
64 this.onAccessibleWalkerFrontDestroyed =
65 this.onAccessibleWalkerFrontDestroyed.bind(this);
66 this.unhighlightBeforeCalling = this.unhighlightBeforeCalling.bind(this);
67 this.toggleDisplayTabbingOrder = this.toggleDisplayTabbingOrder.bind(this);
70 get enabled() {
71 return this.accessibilityFront && this.accessibilityFront.enabled;
74 /**
75 * Indicates whether the accessibility service is enabled.
77 get canBeEnabled() {
78 return this.parentAccessibilityFront.canBeEnabled;
81 get currentTarget() {
82 return this.commands.targetCommand.selectedTargetFront;
85 /**
86 * Perform an audit for a given filter.
88 * @param {String} filter
89 * Type of an audit to perform.
90 * @param {Function} onProgress
91 * Audit progress callback.
93 * @return {Promise}
94 * Resolves when the audit for every document, that each of the frame
95 * accessibility walkers traverse, completes.
97 async audit(filter, onProgress) {
98 const types = filter === FILTERS.ALL ? Object.values(AUDIT_TYPE) : [filter];
100 const targetTypes = [this.commands.targetCommand.TYPES.FRAME];
101 const targets =
102 await this.commands.targetCommand.getAllTargetsInSelectedTargetTree(
103 targetTypes
106 const progress = new CombinedProgress({
107 onProgress,
108 totalFrames: targets.length,
110 const audits = await this.withAllAccessibilityWalkerFronts(
111 async accessibleWalkerFront =>
112 accessibleWalkerFront.audit({
113 types,
114 onProgress: progress.onProgressForWalker.bind(
115 progress,
116 accessibleWalkerFront
118 // If a frame was selected in the iframe picker, we don't want to retrieve the
119 // ancestries at it would mess with the tree structure and would make it misbehave.
120 retrieveAncestries:
121 this.commands.targetCommand.isTopLevelTargetSelected(),
125 // Accumulate all audits into a single structure.
126 const combinedAudit = { ancestries: [] };
127 for (const audit of audits) {
128 // If any of the audits resulted in an error, no need to continue.
129 if (audit.error) {
130 return audit;
133 combinedAudit.ancestries.push(...audit.ancestries);
136 return combinedAudit;
139 async toggleDisplayTabbingOrder(displayTabbingOrder) {
140 if (displayTabbingOrder) {
141 const { walker: domWalkerFront } =
142 await this.currentTarget.getFront("inspector");
143 await this.accessibilityFront.accessibleWalkerFront.showTabbingOrder(
144 await domWalkerFront.getRootNode(),
147 } else {
148 // we don't want to use withAllAccessibilityWalkerFronts as it only acts on selected
149 // target tree, and we want to hide _all_ highlighters.
150 const accessibilityFronts =
151 await this.commands.targetCommand.getAllFronts(
152 [this.commands.targetCommand.TYPES.FRAME],
153 "accessibility"
155 await Promise.all(
156 accessibilityFronts.map(accessibilityFront =>
157 accessibilityFront.accessibleWalkerFront.hideTabbingOrder()
163 async enableAccessibility() {
164 // Accessibility service is initialized using the parent accessibility
165 // front. That, in turn, initializes accessibility service in all content
166 // processes. We need to wait until that happens to be sure platform
167 // accessibility is fully enabled.
168 const enabled = this.accessibilityFront.once("init");
169 await this.parentAccessibilityFront.enable();
170 await enabled;
174 * Return the topmost level accessibility walker to be used as the root of
175 * the accessibility tree view.
177 * @return {Object}
178 * Topmost accessibility walker.
180 getAccessibilityTreeRoot() {
181 return this.accessibilityFront.accessibleWalkerFront;
185 * Look up accessibility fronts (get an existing one or create a new one) for
186 * all existing target fronts and run a task with each one of them.
187 * @param {Function} task
188 * Function to execute with each accessiblity front.
190 async withAllAccessibilityFronts(taskFn) {
191 const accessibilityFronts = await this.commands.targetCommand.getAllFronts(
192 [this.commands.targetCommand.TYPES.FRAME],
193 "accessibility",
195 // only get the fronts for the selected frame tree, in case a specific document
196 // is selected in the iframe picker (if not, the top-level target is considered
197 // as the selected target)
198 onlyInSelectedTargetTree: true,
201 const tasks = [];
202 for (const accessibilityFront of accessibilityFronts) {
203 tasks.push(taskFn(accessibilityFront));
206 return Promise.all(tasks);
210 * Look up accessibility walker fronts (get an existing one or create a new
211 * one using accessibility front) for all existing target fronts and run a
212 * task with each one of them.
213 * @param {Function} task
214 * Function to execute with each accessiblity walker front.
216 withAllAccessibilityWalkerFronts(taskFn) {
217 return this.withAllAccessibilityFronts(async accessibilityFront =>
218 taskFn(accessibilityFront.accessibleWalkerFront)
223 * Unhighlight previous accessible object if we switched between processes and
224 * call the appropriate event handler.
226 unhighlightBeforeCalling(listener) {
227 return async accessible => {
228 if (accessible) {
229 const accessibleWalkerFront = accessible.getParent();
230 if (this._currentAccessibleWalkerFront !== accessibleWalkerFront) {
231 if (this._currentAccessibleWalkerFront) {
232 await this._currentAccessibleWalkerFront.unhighlight();
235 this._currentAccessibleWalkerFront = accessibleWalkerFront;
239 await listener(accessible);
244 * Start picking and add walker listeners.
245 * @param {Boolean} doFocus
246 * If true, move keyboard focus into content.
248 pick(doFocus, onHovered, onPicked, onPreviewed, onCanceled) {
249 return this.withAllAccessibilityWalkerFronts(
250 async accessibleWalkerFront => {
251 this.startListening(accessibleWalkerFront, {
252 events: {
253 "picker-accessible-hovered":
254 this.unhighlightBeforeCalling(onHovered),
255 "picker-accessible-picked": this.unhighlightBeforeCalling(onPicked),
256 "picker-accessible-previewed":
257 this.unhighlightBeforeCalling(onPreviewed),
258 "picker-accessible-canceled":
259 this.unhighlightBeforeCalling(onCanceled),
261 // Only register listeners once (for top level), no need to register
262 // them for all walkers again and again.
263 register: accessibleWalkerFront.targetFront.isTopLevel,
265 await accessibleWalkerFront.pick(
266 // Only pass doFocus to the top level accessibility walker front.
267 doFocus && accessibleWalkerFront.targetFront.isTopLevel
274 * Stop picking and remove all walker listeners.
276 async cancelPick() {
277 this._currentAccessibleWalkerFront = null;
278 return this.withAllAccessibilityWalkerFronts(
279 async accessibleWalkerFront => {
280 await accessibleWalkerFront.cancelPick();
281 this.stopListening(accessibleWalkerFront, {
282 events: {
283 "picker-accessible-hovered": null,
284 "picker-accessible-picked": null,
285 "picker-accessible-previewed": null,
286 "picker-accessible-canceled": null,
288 // Only unregister listeners once (for top level), no need to
289 // unregister them for all walkers again and again.
290 unregister: accessibleWalkerFront.targetFront.isTopLevel,
296 async resetAccessiblity() {
297 const { enabled } = this.accessibilityFront;
298 const { canBeEnabled, canBeDisabled } = this.parentAccessibilityFront;
299 return { enabled, canBeDisabled, canBeEnabled };
302 startListening(front, { events, register = false } = {}) {
303 for (const [type, listener] of Object.entries(events)) {
304 front.on(type, listener);
305 if (register) {
306 this.registerEvent(front, type, listener);
311 stopListening(front, { events, unregister = false } = {}) {
312 for (const [type, listener] of Object.entries(events)) {
313 front.off(type, listener);
314 if (unregister) {
315 this.unregisterEvent(front, type, listener);
320 startListeningForAccessibilityEvents(events) {
321 for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) {
322 this.startListening(accessibleWalkerFront, {
323 events,
324 // Only register listeners once (for top level), no need to register
325 // them for all walkers again and again.
326 register: accessibleWalkerFront.targetFront.isTopLevel,
331 stopListeningForAccessibilityEvents(events) {
332 for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) {
333 this.stopListening(accessibleWalkerFront, {
334 events,
335 // Only unregister listeners once (for top level), no need to unregister
336 // them for all walkers again and again.
337 unregister: accessibleWalkerFront.targetFront.isTopLevel,
342 startListeningForLifecycleEvents(events) {
343 this.startListening(this.accessibilityFront, { events, register: true });
346 stopListeningForLifecycleEvents(events) {
347 this.stopListening(this.accessibilityFront, { events, unregister: true });
350 startListeningForParentLifecycleEvents(events) {
351 this.startListening(this.parentAccessibilityFront, {
352 events,
353 register: false,
357 stopListeningForParentLifecycleEvents(events) {
358 this.stopListening(this.parentAccessibilityFront, {
359 events,
360 unregister: false,
364 highlightAccessible(accessibleFront, options) {
365 if (!accessibleFront) {
366 return;
369 const accessibleWalkerFront = accessibleFront.getParent();
370 if (!accessibleWalkerFront) {
371 return;
374 accessibleWalkerFront
375 .highlightAccessible(accessibleFront, options)
376 .catch(error => {
377 // Only report an error where there's still a commands instance.
378 // Ignore cases where toolbox is already destroyed.
379 if (this.commands) {
380 console.error(error);
385 unhighlightAccessible(accessibleFront) {
386 if (!accessibleFront) {
387 return;
390 const accessibleWalkerFront = accessibleFront.getParent();
391 if (!accessibleWalkerFront) {
392 return;
395 accessibleWalkerFront.unhighlight().catch(error => {
396 // Only report an error where there's still a commands instance.
397 // Ignore cases where toolbox is already destroyed.
398 if (this.commands) {
399 console.error(error);
404 async initialize() {
405 // Initialize it first as it may be used on target selection when calling watchTargets
406 this.parentAccessibilityFront =
407 await this.commands.targetCommand.rootFront.getFront(
408 "parentaccessibility"
411 await this.commands.targetCommand.watchTargets({
412 types: [this.commands.targetCommand.TYPES.FRAME],
413 onAvailable: this.onTargetAvailable,
414 onSelected: this.onTargetSelected,
415 onDestroyed: this.onTargetDestroyed,
418 // Enable accessibility service if necessary.
419 if (this.canBeEnabled && !this.enabled) {
420 await this.enableAccessibility();
422 this.#initialized = true;
425 get supports() {
426 // Retrieve backward compatibility traits.
427 // New API's must be described in the "getTraits" method of the AccessibilityActor.
428 return this.accessibilityFront.traits;
431 destroy() {
432 this.commands.targetCommand.unwatchTargets({
433 types: [this.commands.targetCommand.TYPES.FRAME],
434 onAvailable: this.onTargetAvailable,
435 onSelected: this.onTargetSelected,
436 onDestroyed: this.onTargetDestroyed,
439 this.lifecycleEvents.clear();
440 this.accessibilityEvents.clear();
442 this.accessibilityFront = null;
443 this.parentAccessibilityFront = null;
444 this.simulatorFront = null;
445 this.simulate = null;
446 this.commands = null;
449 _getEvents(front) {
450 return front.typeName === "accessiblewalker"
451 ? this.accessibilityEvents
452 : this.lifecycleEvents;
455 registerEvent(front, type, listener) {
456 const events = this._getEvents(front);
457 if (events.has(type)) {
458 events.get(type).add(listener);
459 } else {
460 events.set(type, new Set([listener]));
464 unregisterEvent(front, type, listener) {
465 const events = this._getEvents(front);
466 if (!events.has(type)) {
467 return;
470 if (!listener) {
471 events.delete(type);
472 return;
475 const listeners = events.get(type);
476 if (listeners.has(listener)) {
477 listeners.delete(listener);
480 if (!listeners.size) {
481 events.delete(type);
485 onAccessibilityFrontAvailable(accessibilityFront) {
486 accessibilityFront.watchFronts(
487 "accessiblewalker",
488 this.onAccessibleWalkerFrontAvailable,
489 this.onAccessibleWalkerFrontDestroyed
493 onAccessibilityFrontDestroyed(accessibilityFront) {
494 accessibilityFront.unwatchFronts(
495 "accessiblewalker",
496 this.onAccessibleWalkerFrontAvailable,
497 this.onAccessibleWalkerFrontDestroyed
501 onAccessibleWalkerFrontAvailable(accessibleWalkerFront) {
502 this._accessibilityWalkerFronts.add(accessibleWalkerFront);
503 // Apply all existing accessible walker front event listeners to the new
504 // front.
505 for (const [type, listeners] of this.accessibilityEvents.entries()) {
506 for (const listener of listeners) {
507 accessibleWalkerFront.on(type, listener);
512 onAccessibleWalkerFrontDestroyed(accessibleWalkerFront) {
513 this._accessibilityWalkerFronts.delete(accessibleWalkerFront);
514 // Remove all existing accessible walker front event listeners from the
515 // destroyed front.
516 for (const [type, listeners] of this.accessibilityEvents.entries()) {
517 for (const listener of listeners) {
518 accessibleWalkerFront.off(type, listener);
523 async onTargetAvailable({ targetFront }) {
524 targetFront.watchFronts(
525 "accessibility",
526 this.onAccessibilityFrontAvailable,
527 this.onAccessibilityFrontDestroyed
530 if (!targetFront.isTopLevel) {
531 return;
534 // Clear all the fronts collected by `watchFronts` on the previous set of targets/documents.
535 this._accessibilityWalkerFronts.clear();
538 async onTargetDestroyed({ targetFront }) {
539 targetFront.unwatchFronts(
540 "accessibility",
541 this.onAccessibilityFrontAvailable,
542 this.onAccessibilityFrontDestroyed
546 async onTargetSelected({ targetFront }) {
547 this.accessibilityFront = await targetFront.getFront("accessibility");
549 this.simulatorFront = this.accessibilityFront.simulatorFront;
550 if (this.simulatorFront) {
551 this.simulate = types => this.simulatorFront.simulate({ types });
552 } else {
553 this.simulate = null;
556 await this.toggleDisplayTabbingOrder(false);
558 // Move accessibility front lifecycle event listeners to a new top level
559 // front.
560 for (const [type, listeners] of this.lifecycleEvents.entries()) {
561 for (const listener of listeners.values()) {
562 this.accessibilityFront.on(type, listener);
566 // Hold on refreshing the view on initialization.
567 // This will be done by the Panel class after everything is setup.
568 // (we especially need to wait for the a11y service to be started)
569 if (this.#initialized) {
570 await this.#panel.forceRefresh();
575 exports.AccessibilityProxy = AccessibilityProxy;