accessibility.js (18164B)
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 const { 8 FrontClassWithSpec, 9 registerFront, 10 } = require("resource://devtools/shared/protocol.js"); 11 const { 12 accessibleSpec, 13 accessibleWalkerSpec, 14 accessibilitySpec, 15 parentAccessibilitySpec, 16 simulatorSpec, 17 } = require("resource://devtools/shared/specs/accessibility.js"); 18 19 class AccessibleFront extends FrontClassWithSpec(accessibleSpec) { 20 constructor(client, targetFront, parentFront) { 21 super(client, targetFront, parentFront); 22 23 this.before("audited", this.audited.bind(this)); 24 this.before("name-change", this.nameChange.bind(this)); 25 this.before("value-change", this.valueChange.bind(this)); 26 this.before("description-change", this.descriptionChange.bind(this)); 27 this.before("shortcut-change", this.shortcutChange.bind(this)); 28 this.before("reorder", this.reorder.bind(this)); 29 this.before("text-change", this.textChange.bind(this)); 30 this.before("index-in-parent-change", this.indexInParentChange.bind(this)); 31 this.before("states-change", this.statesChange.bind(this)); 32 this.before("actions-change", this.actionsChange.bind(this)); 33 this.before("attributes-change", this.attributesChange.bind(this)); 34 } 35 36 marshallPool() { 37 return this.getParent(); 38 } 39 40 get useChildTargetToFetchChildren() { 41 return this._form.useChildTargetToFetchChildren; 42 } 43 44 get role() { 45 return this._form.role; 46 } 47 48 get name() { 49 return this._form.name; 50 } 51 52 get value() { 53 return this._form.value; 54 } 55 56 get description() { 57 return this._form.description; 58 } 59 60 get keyboardShortcut() { 61 return this._form.keyboardShortcut; 62 } 63 64 get childCount() { 65 return this._form.childCount; 66 } 67 68 get domNodeType() { 69 return this._form.domNodeType; 70 } 71 72 get indexInParent() { 73 return this._form.indexInParent; 74 } 75 76 get states() { 77 return this._form.states; 78 } 79 80 get actions() { 81 return this._form.actions; 82 } 83 84 get attributes() { 85 return this._form.attributes; 86 } 87 88 get checks() { 89 return this._form.checks; 90 } 91 92 form(form) { 93 this.actorID = form.actor; 94 this._form = this._form || {}; 95 Object.assign(this._form, form); 96 } 97 98 nameChange(name, parent) { 99 this._form.name = name; 100 // Name change event affects the tree rendering, we fire this event on 101 // accessibility walker as the point of interaction for UI. 102 const accessibilityWalkerFront = this.getParent(); 103 if (accessibilityWalkerFront) { 104 accessibilityWalkerFront.emit("name-change", this, parent); 105 } 106 } 107 108 valueChange(value) { 109 this._form.value = value; 110 } 111 112 descriptionChange(description) { 113 this._form.description = description; 114 } 115 116 shortcutChange(keyboardShortcut) { 117 this._form.keyboardShortcut = keyboardShortcut; 118 } 119 120 reorder(childCount) { 121 this._form.childCount = childCount; 122 // Reorder event affects the tree rendering, we fire this event on 123 // accessibility walker as the point of interaction for UI. 124 const accessibilityWalkerFront = this.getParent(); 125 if (accessibilityWalkerFront) { 126 accessibilityWalkerFront.emit("reorder", this); 127 } 128 } 129 130 textChange() { 131 // Text event affects the tree rendering, we fire this event on 132 // accessibility walker as the point of interaction for UI. 133 const accessibilityWalkerFront = this.getParent(); 134 if (accessibilityWalkerFront) { 135 accessibilityWalkerFront.emit("text-change", this); 136 } 137 } 138 139 indexInParentChange(indexInParent) { 140 this._form.indexInParent = indexInParent; 141 } 142 143 statesChange(states) { 144 this._form.states = states; 145 } 146 147 actionsChange(actions) { 148 this._form.actions = actions; 149 } 150 151 attributesChange(attributes) { 152 this._form.attributes = attributes; 153 } 154 155 audited(checks) { 156 this._form.checks = this._form.checks || {}; 157 Object.assign(this._form.checks, checks); 158 } 159 160 hydrate() { 161 return super.hydrate().then(properties => { 162 Object.assign(this._form, properties); 163 }); 164 } 165 166 async children() { 167 if (!this.useChildTargetToFetchChildren) { 168 return super.children(); 169 } 170 171 const { walker: domWalkerFront } = 172 await this.targetFront.getFront("inspector"); 173 const node = await domWalkerFront.getNodeFromActor(this.actorID, [ 174 "rawAccessible", 175 "DOMNode", 176 ]); 177 // We are using DOM inspector/walker API here because we want to keep both 178 // the accessiblity tree and the DOM tree in sync. This is necessary for 179 // several features that the accessibility panel provides such as inspecting 180 // a corresponding DOM node or any other functionality that requires DOM 181 // node ancestries to be resolved all the way up to the top level document. 182 const { 183 nodes: [documentNodeFront], 184 } = await domWalkerFront.children(node); 185 const accessibilityFront = 186 await documentNodeFront.targetFront.getFront("accessibility"); 187 188 return accessibilityFront.accessibleWalkerFront.children(); 189 } 190 191 /** 192 * Helper function that helps with building a complete snapshot of 193 * accessibility tree starting at the level of current accessible front. It 194 * accumulates subtrees from possible out of process frames that are children 195 * of the current accessible front. 196 * 197 * @param {JSON} snapshot 198 * Snapshot of the current accessible front or one of its in process 199 * children when recursing. 200 * 201 * @return {JSON} 202 * Complete snapshot of current accessible front. 203 */ 204 async _accumulateSnapshot(snapshot) { 205 const { childCount, useChildTargetToFetchChildren } = snapshot; 206 // No children, we are done. 207 if (childCount === 0) { 208 return snapshot; 209 } 210 211 // If current accessible is not a remote frame, continue accumulating inside 212 // its children. 213 if (!useChildTargetToFetchChildren) { 214 const childSnapshots = []; 215 for (const childSnapshot of snapshot.children) { 216 childSnapshots.push(this._accumulateSnapshot(childSnapshot)); 217 } 218 await Promise.all(childSnapshots); 219 return snapshot; 220 } 221 222 // When we have a remote frame, we need to obtain an accessible front for a 223 // remote frame document and retrieve its snapshot. 224 const inspectorFront = await this.targetFront.getFront("inspector"); 225 const frameNodeFront = 226 await inspectorFront.getNodeActorFromContentDomReference( 227 snapshot.contentDOMReference 228 ); 229 // Remove contentDOMReference and useChildTargetToFetchChildren properties. 230 delete snapshot.contentDOMReference; 231 delete snapshot.useChildTargetToFetchChildren; 232 if (!frameNodeFront) { 233 return snapshot; 234 } 235 236 // Remote frame lives in the same process as the current accessible 237 // front we can retrieve the accessible front directly. 238 const frameAccessibleFront = 239 await this.parentFront.getAccessibleFor(frameNodeFront); 240 if (!frameAccessibleFront) { 241 return snapshot; 242 } 243 244 const [docAccessibleFront] = await frameAccessibleFront.children(); 245 const childSnapshot = await docAccessibleFront.snapshot(); 246 snapshot.children.push(childSnapshot); 247 248 return snapshot; 249 } 250 251 /** 252 * Retrieves a complete JSON snapshot for an accessible subtree of a given 253 * accessible front (inclduing OOP frames). 254 */ 255 async snapshot() { 256 const snapshot = await super.snapshot(); 257 await this._accumulateSnapshot(snapshot); 258 return snapshot; 259 } 260 } 261 262 class AccessibleWalkerFront extends FrontClassWithSpec(accessibleWalkerSpec) { 263 constructor(client, targetFront, parentFront) { 264 super(client, targetFront, parentFront); 265 266 this.documentReady = this.documentReady.bind(this); 267 this.on("document-ready", this.documentReady); 268 } 269 270 destroy() { 271 this.off("document-ready", this.documentReady); 272 super.destroy(); 273 } 274 275 form(json) { 276 this.actorID = json.actor; 277 } 278 279 documentReady() { 280 if (this.targetFront.isTopLevel) { 281 this.emit("top-level-document-ready"); 282 } 283 } 284 285 pick(doFocus) { 286 if (doFocus) { 287 return this.pickAndFocus(); 288 } 289 290 return super.pick(); 291 } 292 293 /** 294 * Get the accessible object ancestry starting from the given accessible to 295 * the top level document. The top level document is in the top level content process. 296 * 297 * @param {object} accessible 298 * Accessible front to determine the ancestry for. 299 * 300 * @return {Array} ancestry 301 * List of ancestry objects which consist of an accessible with its 302 * children. 303 */ 304 async getAncestry(accessible) { 305 const ancestry = await super.getAncestry(accessible); 306 307 const parentTarget = await this.targetFront.getParentTarget(); 308 if (!parentTarget) { 309 return ancestry; 310 } 311 312 // Get an accessible front for the parent frame. We go through the 313 // inspector's walker to keep both inspector and accessibility trees in 314 // sync. 315 const { walker: domWalkerFront } = 316 await this.targetFront.getFront("inspector"); 317 const frameNodeFront = (await domWalkerFront.getRootNode()).parentNode(); 318 const accessibilityFront = await parentTarget.getFront("accessibility"); 319 const { accessibleWalkerFront } = accessibilityFront; 320 const frameAccessibleFront = 321 await accessibleWalkerFront.getAccessibleFor(frameNodeFront); 322 323 if (!frameAccessibleFront) { 324 // Most likely we are inside a hidden frame. 325 return Promise.reject( 326 `Can't get the ancestry for an accessible front ${accessible.actorID}. It is in the detached tree.` 327 ); 328 } 329 330 // Compose the final ancestry out of ancestry for the given accessible in 331 // the current process and recursively get the ancestry for the frame 332 // accessible. 333 ancestry.push( 334 { 335 accessible: frameAccessibleFront, 336 children: await frameAccessibleFront.children(), 337 }, 338 ...(await accessibleWalkerFront.getAncestry(frameAccessibleFront)) 339 ); 340 341 return ancestry; 342 } 343 344 /** 345 * Run an accessibility audit for a document that accessibility walker is 346 * responsible for (in process). In addition to plainly running an audit (in 347 * cases when the document is in the OOP frame), this method also updates 348 * relative ancestries of audited accessible objects all the way up to the top 349 * level document for the toolbox. 350 * 351 * @param {object} options 352 * - {Array} types 353 * types of the accessibility issues to audit for 354 * - {Function} onProgress 355 * callback function for a progress audit-event 356 * - {Boolean} retrieveAncestries (defaults to true) 357 * Set to false to _not_ retrieve ancestries of audited accessible objects. 358 * This is used when a specific document is selected in the iframe picker 359 * and we want to treat it as the root of the accessibility panel tree. 360 */ 361 async audit({ types, onProgress, retrieveAncestries = true }) { 362 const onAudit = new Promise(resolve => { 363 const auditEventHandler = ({ type, ancestries, progress }) => { 364 switch (type) { 365 case "error": 366 this.off("audit-event", auditEventHandler); 367 resolve({ error: true }); 368 break; 369 case "completed": 370 this.off("audit-event", auditEventHandler); 371 resolve({ ancestries }); 372 break; 373 case "progress": 374 onProgress(progress); 375 break; 376 default: 377 break; 378 } 379 }; 380 381 this.on("audit-event", auditEventHandler); 382 super.startAudit({ types }); 383 }); 384 385 const audit = await onAudit; 386 // If audit resulted in an error, if there's nothing to report or if the callsite 387 // explicitly asked to not retrieve ancestries, we are done. 388 // (no need to check for ancestry across the remote frame hierarchy). 389 // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1641551 why the rest of 390 // the code path is only supported when content toolbox fission is enabled. 391 if (audit.error || audit.ancestries.length === 0 || !retrieveAncestries) { 392 return audit; 393 } 394 395 const parentTarget = await this.targetFront.getParentTarget(); 396 // If there is no parent target, we do not need to update ancestries as we 397 // are in the top level document. 398 if (!parentTarget) { 399 return audit; 400 } 401 402 // Retrieve an ancestry (cross process) for a current root document and make 403 // audit report ancestries relative to it. 404 const [docAccessibleFront] = await this.children(); 405 let docAccessibleAncestry; 406 try { 407 docAccessibleAncestry = await this.getAncestry(docAccessibleFront); 408 } catch (e) { 409 // We are in a detached subtree. We do not consider this an error, instead 410 // we need to ignore the audit for this frame and return an empty report. 411 return { ancestries: [] }; 412 } 413 for (const ancestry of audit.ancestries) { 414 // Compose the final ancestries out of the ones in the audit report 415 // relative to this document and the ancestry of the document itself 416 // (cross process). 417 ancestry.push(...docAccessibleAncestry); 418 } 419 420 return audit; 421 } 422 423 /** 424 * A helper wrapper function to show tabbing order overlay for a given target. 425 * The only additional work done is resolving domnode front from a 426 * ContentDOMReference received from a remote target. 427 * 428 * @param {object} startElm 429 * domnode front to be used as the starting point for generating the 430 * tabbing order. 431 * @param {number} startIndex 432 * Starting index for the tabbing order. 433 */ 434 async _showTabbingOrder(startElm, startIndex) { 435 const { contentDOMReference, index } = await super.showTabbingOrder( 436 startElm, 437 startIndex 438 ); 439 let elm; 440 if (contentDOMReference) { 441 const inspectorFront = await this.targetFront.getFront("inspector"); 442 elm = 443 await inspectorFront.getNodeActorFromContentDomReference( 444 contentDOMReference 445 ); 446 } 447 448 return { elm, index }; 449 } 450 451 /** 452 * Show tabbing order overlay for a given target. 453 * 454 * @param {object} startElm 455 * domnode front to be used as the starting point for generating the 456 * tabbing order. 457 * @param {number} startIndex 458 * Starting index for the tabbing order. 459 * 460 * @return {JSON} 461 * Tabbing order information for the last element in the tabbing 462 * order. It includes a domnode front and a tabbing index. If we are 463 * at the end of the tabbing order for the top level content document, 464 * the domnode front will be null. If focus manager discovered a 465 * remote IFRAME, then the domnode front is for the IFRAME itself. 466 */ 467 async showTabbingOrder(startElm, startIndex) { 468 let { elm: currentElm, index: currentIndex } = await this._showTabbingOrder( 469 startElm, 470 startIndex 471 ); 472 473 // If no remote frames were found, currentElm will be null. 474 while (currentElm) { 475 // Safety check to ensure that the currentElm is a remote frame. 476 if (currentElm.useChildTargetToFetchChildren) { 477 const { walker: domWalkerFront } = 478 await currentElm.targetFront.getFront("inspector"); 479 const { 480 nodes: [childDocumentNodeFront], 481 } = await domWalkerFront.children(currentElm); 482 const { accessibleWalkerFront } = 483 await childDocumentNodeFront.targetFront.getFront("accessibility"); 484 // Show tabbing order in the remote target, while updating the tabbing 485 // index. 486 ({ index: currentIndex } = await accessibleWalkerFront.showTabbingOrder( 487 childDocumentNodeFront, 488 currentIndex 489 )); 490 } 491 492 // Finished with the remote frame, continue in tabbing order, from the 493 // remote frame. 494 ({ elm: currentElm, index: currentIndex } = await this._showTabbingOrder( 495 currentElm, 496 currentIndex 497 )); 498 } 499 500 return { elm: currentElm, index: currentIndex }; 501 } 502 } 503 504 class AccessibilityFront extends FrontClassWithSpec(accessibilitySpec) { 505 constructor(client, targetFront, parentFront) { 506 super(client, targetFront, parentFront); 507 508 this.before("init", this.init.bind(this)); 509 this.before("shutdown", this.shutdown.bind(this)); 510 511 // Attribute name from which to retrieve the actorID out of the target 512 // actor's form 513 this.formAttributeName = "accessibilityActor"; 514 } 515 516 async initialize() { 517 this.accessibleWalkerFront = await super.getWalker(); 518 this.simulatorFront = await super.getSimulator(); 519 const { enabled } = await super.bootstrap(); 520 this.enabled = enabled; 521 522 try { 523 this._traits = await this.getTraits(); 524 } catch (e) { 525 // @backward-compat { version 84 } getTraits isn't available on older server. 526 this._traits = {}; 527 } 528 } 529 530 get traits() { 531 return this._traits; 532 } 533 534 init() { 535 this.enabled = true; 536 } 537 538 shutdown() { 539 this.enabled = false; 540 } 541 } 542 543 class ParentAccessibilityFront extends FrontClassWithSpec( 544 parentAccessibilitySpec 545 ) { 546 constructor(client, targetFront, parentFront) { 547 super(client, targetFront, parentFront); 548 this.before("can-be-enabled-change", this.canBeEnabled.bind(this)); 549 this.before("can-be-disabled-change", this.canBeDisabled.bind(this)); 550 551 // Attribute name from which to retrieve the actorID out of the target 552 // actor's form 553 this.formAttributeName = "parentAccessibilityActor"; 554 } 555 556 async initialize() { 557 ({ canBeEnabled: this.canBeEnabled, canBeDisabled: this.canBeDisabled } = 558 await super.bootstrap()); 559 } 560 561 canBeEnabled(canBeEnabled) { 562 this.canBeEnabled = canBeEnabled; 563 } 564 565 canBeDisabled(canBeDisabled) { 566 this.canBeDisabled = canBeDisabled; 567 } 568 } 569 570 const SimulatorFront = FrontClassWithSpec(simulatorSpec); 571 572 registerFront(AccessibleFront); 573 registerFront(AccessibleWalkerFront); 574 registerFront(AccessibilityFront); 575 registerFront(ParentAccessibilityFront); 576 registerFront(SimulatorFront);