accessibility-proxy.js (18697B)
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 5 "use strict"; 6 7 loader.lazyRequireGetter( 8 this, 9 "CombinedProgress", 10 "resource://devtools/client/accessibility/utils/audit.js", 11 true 12 ); 13 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"); 20 21 /** 22 * Component responsible for tracking all Accessibility fronts in parent and 23 * content processes. 24 */ 25 class AccessibilityProxy { 26 #panel; 27 #initialized; 28 constructor(commands, panel) { 29 this.commands = commands; 30 this.#panel = panel; 31 32 this.#initialized = false; 33 this._accessibilityWalkerFronts = new Set(); 34 this.lifecycleEvents = new Map(); 35 this.accessibilityEvents = new Map(); 36 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); 68 } 69 70 get enabled() { 71 return this.accessibilityFront && this.accessibilityFront.enabled; 72 } 73 74 /** 75 * Indicates whether the accessibility service is enabled. 76 */ 77 get canBeEnabled() { 78 return this.parentAccessibilityFront.canBeEnabled; 79 } 80 81 get currentTarget() { 82 return this.commands.targetCommand.selectedTargetFront; 83 } 84 85 /** 86 * Perform an audit for a given filter. 87 * 88 * @param {string} filter 89 * Type of an audit to perform. 90 * @param {Function} onProgress 91 * Audit progress callback. 92 * 93 * @return {Promise} 94 * Resolves when the audit for every document, that each of the frame 95 * accessibility walkers traverse, completes. 96 */ 97 async audit(filter, onProgress) { 98 const types = filter === FILTERS.ALL ? Object.values(AUDIT_TYPE) : [filter]; 99 100 const targetTypes = [this.commands.targetCommand.TYPES.FRAME]; 101 const targets = 102 await this.commands.targetCommand.getAllTargetsInSelectedTargetTree( 103 targetTypes 104 ); 105 106 const progress = new CombinedProgress({ 107 onProgress, 108 totalFrames: targets.length, 109 }); 110 const audits = await this.withAllAccessibilityWalkerFronts( 111 async accessibleWalkerFront => 112 accessibleWalkerFront.audit({ 113 types, 114 onProgress: progress.onProgressForWalker.bind( 115 progress, 116 accessibleWalkerFront 117 ), 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(), 122 }) 123 ); 124 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; 131 } 132 133 combinedAudit.ancestries.push(...audit.ancestries); 134 } 135 136 return combinedAudit; 137 } 138 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(), 145 0 146 ); 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" 154 ); 155 await Promise.all( 156 accessibilityFronts.map(accessibilityFront => 157 accessibilityFront.accessibleWalkerFront.hideTabbingOrder() 158 ) 159 ); 160 } 161 } 162 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; 171 } 172 173 /** 174 * Return the topmost level accessibility walker to be used as the root of 175 * the accessibility tree view. 176 * 177 * @return {object} 178 * Topmost accessibility walker. 179 */ 180 getAccessibilityTreeRoot() { 181 return this.accessibilityFront.accessibleWalkerFront; 182 } 183 184 /** 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 * 188 * @param {Function} task 189 * Function to execute with each accessiblity front. 190 */ 191 async withAllAccessibilityFronts(taskFn) { 192 const accessibilityFronts = await this.commands.targetCommand.getAllFronts( 193 [this.commands.targetCommand.TYPES.FRAME], 194 "accessibility", 195 { 196 // only get the fronts for the selected frame tree, in case a specific document 197 // is selected in the iframe picker (if not, the top-level target is considered 198 // as the selected target) 199 onlyInSelectedTargetTree: true, 200 } 201 ); 202 const tasks = []; 203 for (const accessibilityFront of accessibilityFronts) { 204 tasks.push(taskFn(accessibilityFront)); 205 } 206 207 return Promise.all(tasks); 208 } 209 210 /** 211 * Look up accessibility walker fronts (get an existing one or create a new 212 * one using accessibility front) for all existing target fronts and run a 213 * task with each one of them. 214 * 215 * @param {Function} task 216 * Function to execute with each accessiblity walker front. 217 */ 218 withAllAccessibilityWalkerFronts(taskFn) { 219 return this.withAllAccessibilityFronts(async accessibilityFront => 220 taskFn(accessibilityFront.accessibleWalkerFront) 221 ); 222 } 223 224 /** 225 * Unhighlight previous accessible object if we switched between processes and 226 * call the appropriate event handler. 227 */ 228 unhighlightBeforeCalling(listener) { 229 return async accessible => { 230 if (accessible) { 231 const accessibleWalkerFront = accessible.getParent(); 232 if (this._currentAccessibleWalkerFront !== accessibleWalkerFront) { 233 if (this._currentAccessibleWalkerFront) { 234 await this._currentAccessibleWalkerFront.unhighlight(); 235 } 236 237 this._currentAccessibleWalkerFront = accessibleWalkerFront; 238 } 239 } 240 241 await listener(accessible); 242 }; 243 } 244 245 /** 246 * Start picking and add walker listeners. 247 * 248 * @param {boolean} doFocus 249 * If true, move keyboard focus into content. 250 */ 251 pick(doFocus, onHovered, onPicked, onPreviewed, onCanceled) { 252 return this.withAllAccessibilityWalkerFronts( 253 async accessibleWalkerFront => { 254 this.startListening(accessibleWalkerFront, { 255 events: { 256 "picker-accessible-hovered": 257 this.unhighlightBeforeCalling(onHovered), 258 "picker-accessible-picked": this.unhighlightBeforeCalling(onPicked), 259 "picker-accessible-previewed": 260 this.unhighlightBeforeCalling(onPreviewed), 261 "picker-accessible-canceled": 262 this.unhighlightBeforeCalling(onCanceled), 263 }, 264 // Only register listeners once (for top level), no need to register 265 // them for all walkers again and again. 266 register: accessibleWalkerFront.targetFront.isTopLevel, 267 }); 268 await accessibleWalkerFront.pick( 269 // Only pass doFocus to the top level accessibility walker front. 270 doFocus && accessibleWalkerFront.targetFront.isTopLevel 271 ); 272 } 273 ); 274 } 275 276 /** 277 * Stop picking and remove all walker listeners. 278 */ 279 async cancelPick() { 280 this._currentAccessibleWalkerFront = null; 281 return this.withAllAccessibilityWalkerFronts( 282 async accessibleWalkerFront => { 283 await accessibleWalkerFront.cancelPick(); 284 this.stopListening(accessibleWalkerFront, { 285 events: { 286 "picker-accessible-hovered": null, 287 "picker-accessible-picked": null, 288 "picker-accessible-previewed": null, 289 "picker-accessible-canceled": null, 290 }, 291 // Only unregister listeners once (for top level), no need to 292 // unregister them for all walkers again and again. 293 unregister: accessibleWalkerFront.targetFront.isTopLevel, 294 }); 295 } 296 ); 297 } 298 299 async resetAccessiblity() { 300 const { enabled } = this.accessibilityFront; 301 const { canBeEnabled, canBeDisabled } = this.parentAccessibilityFront; 302 return { enabled, canBeDisabled, canBeEnabled }; 303 } 304 305 startListening(front, { events, register = false } = {}) { 306 for (const [type, listener] of Object.entries(events)) { 307 front.on(type, listener); 308 if (register) { 309 this.registerEvent(front, type, listener); 310 } 311 } 312 } 313 314 stopListening(front, { events, unregister = false } = {}) { 315 for (const [type, listener] of Object.entries(events)) { 316 front.off(type, listener); 317 if (unregister) { 318 this.unregisterEvent(front, type, listener); 319 } 320 } 321 } 322 323 startListeningForAccessibilityEvents(events) { 324 for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) { 325 this.startListening(accessibleWalkerFront, { 326 events, 327 // Only register listeners once (for top level), no need to register 328 // them for all walkers again and again. 329 register: accessibleWalkerFront.targetFront.isTopLevel, 330 }); 331 } 332 } 333 334 stopListeningForAccessibilityEvents(events) { 335 for (const accessibleWalkerFront of this._accessibilityWalkerFronts.values()) { 336 this.stopListening(accessibleWalkerFront, { 337 events, 338 // Only unregister listeners once (for top level), no need to unregister 339 // them for all walkers again and again. 340 unregister: accessibleWalkerFront.targetFront.isTopLevel, 341 }); 342 } 343 } 344 345 startListeningForLifecycleEvents(events) { 346 this.startListening(this.accessibilityFront, { events, register: true }); 347 } 348 349 stopListeningForLifecycleEvents(events) { 350 this.stopListening(this.accessibilityFront, { events, unregister: true }); 351 } 352 353 startListeningForParentLifecycleEvents(events) { 354 this.startListening(this.parentAccessibilityFront, { 355 events, 356 register: false, 357 }); 358 } 359 360 stopListeningForParentLifecycleEvents(events) { 361 this.stopListening(this.parentAccessibilityFront, { 362 events, 363 unregister: false, 364 }); 365 } 366 367 highlightAccessible(accessibleFront, options) { 368 if (!accessibleFront) { 369 return; 370 } 371 372 const accessibleWalkerFront = accessibleFront.getParent(); 373 if (!accessibleWalkerFront) { 374 return; 375 } 376 377 accessibleWalkerFront 378 .highlightAccessible(accessibleFront, options) 379 .catch(error => { 380 // Only report an error where there's still a commands instance. 381 // Ignore cases where toolbox is already destroyed. 382 if (this.commands) { 383 console.error(error); 384 } 385 }); 386 } 387 388 unhighlightAccessible(accessibleFront) { 389 if (!accessibleFront) { 390 return; 391 } 392 393 const accessibleWalkerFront = accessibleFront.getParent(); 394 if (!accessibleWalkerFront) { 395 return; 396 } 397 398 accessibleWalkerFront.unhighlight().catch(error => { 399 // Only report an error where there's still a commands instance. 400 // Ignore cases where toolbox is already destroyed. 401 if (this.commands) { 402 console.error(error); 403 } 404 }); 405 } 406 407 async initialize() { 408 // Initialize it first as it may be used on target selection when calling watchTargets 409 this.parentAccessibilityFront = 410 await this.commands.targetCommand.rootFront.getFront( 411 "parentaccessibility" 412 ); 413 414 await this.commands.targetCommand.watchTargets({ 415 types: [this.commands.targetCommand.TYPES.FRAME], 416 onAvailable: this.onTargetAvailable, 417 onSelected: this.onTargetSelected, 418 onDestroyed: this.onTargetDestroyed, 419 }); 420 421 // Enable accessibility service if necessary. 422 if (this.canBeEnabled && !this.enabled) { 423 await this.enableAccessibility(); 424 } 425 this.#initialized = true; 426 } 427 428 get supports() { 429 // Retrieve backward compatibility traits. 430 // New API's must be described in the "getTraits" method of the AccessibilityActor. 431 return this.accessibilityFront.traits; 432 } 433 434 destroy() { 435 this.commands.targetCommand.unwatchTargets({ 436 types: [this.commands.targetCommand.TYPES.FRAME], 437 onAvailable: this.onTargetAvailable, 438 onSelected: this.onTargetSelected, 439 onDestroyed: this.onTargetDestroyed, 440 }); 441 442 this.lifecycleEvents.clear(); 443 this.accessibilityEvents.clear(); 444 445 this.accessibilityFront = null; 446 this.parentAccessibilityFront = null; 447 this.simulatorFront = null; 448 this.simulate = null; 449 this.commands = null; 450 } 451 452 _getEvents(front) { 453 return front.typeName === "accessiblewalker" 454 ? this.accessibilityEvents 455 : this.lifecycleEvents; 456 } 457 458 registerEvent(front, type, listener) { 459 const events = this._getEvents(front); 460 if (events.has(type)) { 461 events.get(type).add(listener); 462 } else { 463 events.set(type, new Set([listener])); 464 } 465 } 466 467 unregisterEvent(front, type, listener) { 468 const events = this._getEvents(front); 469 if (!events.has(type)) { 470 return; 471 } 472 473 if (!listener) { 474 events.delete(type); 475 return; 476 } 477 478 const listeners = events.get(type); 479 if (listeners.has(listener)) { 480 listeners.delete(listener); 481 } 482 483 if (!listeners.size) { 484 events.delete(type); 485 } 486 } 487 488 onAccessibilityFrontAvailable(accessibilityFront) { 489 accessibilityFront.watchFronts( 490 "accessiblewalker", 491 this.onAccessibleWalkerFrontAvailable, 492 this.onAccessibleWalkerFrontDestroyed 493 ); 494 } 495 496 onAccessibilityFrontDestroyed(accessibilityFront) { 497 accessibilityFront.unwatchFronts( 498 "accessiblewalker", 499 this.onAccessibleWalkerFrontAvailable, 500 this.onAccessibleWalkerFrontDestroyed 501 ); 502 } 503 504 onAccessibleWalkerFrontAvailable(accessibleWalkerFront) { 505 this._accessibilityWalkerFronts.add(accessibleWalkerFront); 506 // Apply all existing accessible walker front event listeners to the new 507 // front. 508 for (const [type, listeners] of this.accessibilityEvents.entries()) { 509 for (const listener of listeners) { 510 accessibleWalkerFront.on(type, listener); 511 } 512 } 513 } 514 515 onAccessibleWalkerFrontDestroyed(accessibleWalkerFront) { 516 this._accessibilityWalkerFronts.delete(accessibleWalkerFront); 517 // Remove all existing accessible walker front event listeners from the 518 // destroyed front. 519 for (const [type, listeners] of this.accessibilityEvents.entries()) { 520 for (const listener of listeners) { 521 accessibleWalkerFront.off(type, listener); 522 } 523 } 524 } 525 526 async onTargetAvailable({ targetFront }) { 527 targetFront.watchFronts( 528 "accessibility", 529 this.onAccessibilityFrontAvailable, 530 this.onAccessibilityFrontDestroyed 531 ); 532 533 if (!targetFront.isTopLevel) { 534 return; 535 } 536 537 // Clear all the fronts collected by `watchFronts` on the previous set of targets/documents. 538 this._accessibilityWalkerFronts.clear(); 539 } 540 541 async onTargetDestroyed({ targetFront }) { 542 targetFront.unwatchFronts( 543 "accessibility", 544 this.onAccessibilityFrontAvailable, 545 this.onAccessibilityFrontDestroyed 546 ); 547 } 548 549 async onTargetSelected({ targetFront }) { 550 this.accessibilityFront = await targetFront.getFront("accessibility"); 551 552 this.simulatorFront = this.accessibilityFront.simulatorFront; 553 if (this.simulatorFront) { 554 this.simulate = types => this.simulatorFront.simulate({ types }); 555 556 // Re-apply a potential existing simulation 557 const { simulation } = this.#panel.panelWin.view.store.getState(); 558 const simulationType = Object.keys(simulation).find( 559 name => simulation[name] 560 ); 561 if (simulationType) { 562 await this.simulate([simulationType]); 563 } 564 } else { 565 this.simulate = null; 566 } 567 568 await this.toggleDisplayTabbingOrder(false); 569 570 // Move accessibility front lifecycle event listeners to a new top level 571 // front. 572 for (const [type, listeners] of this.lifecycleEvents.entries()) { 573 for (const listener of listeners.values()) { 574 this.accessibilityFront.on(type, listener); 575 } 576 } 577 578 // Hold on refreshing the view on initialization. 579 // This will be done by the Panel class after everything is setup. 580 // (we especially need to wait for the a11y service to be started) 581 if (this.#initialized) { 582 await this.#panel.forceRefresh(); 583 } 584 } 585 } 586 587 exports.AccessibilityProxy = AccessibilityProxy;