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/. */
7 loader
.lazyRequireGetter(
10 "resource://devtools/client/accessibility/utils/audit.js",
15 accessibility
: { AUDIT_TYPE
},
16 } = require("resource://devtools/shared/constants.js");
19 } = require("resource://devtools/client/accessibility/constants.js");
22 * Component responsible for tracking all Accessibility fronts in parent and
25 class AccessibilityProxy
{
28 constructor(commands
, panel
) {
29 this.commands
= commands
;
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);
71 return this.accessibilityFront
&& this.accessibilityFront
.enabled
;
75 * Indicates whether the accessibility service is enabled.
78 return this.parentAccessibilityFront
.canBeEnabled
;
82 return this.commands
.targetCommand
.selectedTargetFront
;
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.
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
];
102 await
this.commands
.targetCommand
.getAllTargetsInSelectedTargetTree(
106 const progress
= new CombinedProgress({
108 totalFrames
: targets
.length
,
110 const audits
= await
this.withAllAccessibilityWalkerFronts(
111 async accessibleWalkerFront
=>
112 accessibleWalkerFront
.audit({
114 onProgress
: progress
.onProgressForWalker
.bind(
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.
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.
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(),
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
],
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();
174 * Return the topmost level accessibility walker to be used as the root of
175 * the accessibility tree view.
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
],
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,
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
=> {
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
, {
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.
277 this._currentAccessibleWalkerFront
= null;
278 return this.withAllAccessibilityWalkerFronts(
279 async accessibleWalkerFront
=> {
280 await accessibleWalkerFront
.cancelPick();
281 this.stopListening(accessibleWalkerFront
, {
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
);
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
);
315 this.unregisterEvent(front
, type
, listener
);
320 startListeningForAccessibilityEvents(events
) {
321 for (const accessibleWalkerFront
of this._accessibilityWalkerFronts
.values()) {
322 this.startListening(accessibleWalkerFront
, {
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
, {
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
, {
357 stopListeningForParentLifecycleEvents(events
) {
358 this.stopListening(this.parentAccessibilityFront
, {
364 highlightAccessible(accessibleFront
, options
) {
365 if (!accessibleFront
) {
369 const accessibleWalkerFront
= accessibleFront
.getParent();
370 if (!accessibleWalkerFront
) {
374 accessibleWalkerFront
375 .highlightAccessible(accessibleFront
, options
)
377 // Only report an error where there's still a commands instance.
378 // Ignore cases where toolbox is already destroyed.
380 console
.error(error
);
385 unhighlightAccessible(accessibleFront
) {
386 if (!accessibleFront
) {
390 const accessibleWalkerFront
= accessibleFront
.getParent();
391 if (!accessibleWalkerFront
) {
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.
399 console
.error(error
);
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;
426 // Retrieve backward compatibility traits.
427 // New API's must be described in the "getTraits" method of the AccessibilityActor.
428 return this.accessibilityFront
.traits
;
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;
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
);
460 events
.set(type
, new Set([listener
]));
464 unregisterEvent(front
, type
, listener
) {
465 const events
= this._getEvents(front
);
466 if (!events
.has(type
)) {
475 const listeners
= events
.get(type
);
476 if (listeners
.has(listener
)) {
477 listeners
.delete(listener
);
480 if (!listeners
.size
) {
485 onAccessibilityFrontAvailable(accessibilityFront
) {
486 accessibilityFront
.watchFronts(
488 this.onAccessibleWalkerFrontAvailable
,
489 this.onAccessibleWalkerFrontDestroyed
493 onAccessibilityFrontDestroyed(accessibilityFront
) {
494 accessibilityFront
.unwatchFronts(
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
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
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(
526 this.onAccessibilityFrontAvailable
,
527 this.onAccessibilityFrontDestroyed
530 if (!targetFront
.isTopLevel
) {
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(
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
});
553 this.simulate
= null;
556 await
this.toggleDisplayTabbingOrder(false);
558 // Move accessibility front lifecycle event listeners to a new top level
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
;