tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

walker.js (38790B)


      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 { Actor } = require("resource://devtools/shared/protocol.js");
      8 const {
      9  accessibleWalkerSpec,
     10 } = require("resource://devtools/shared/specs/accessibility.js");
     11 
     12 const {
     13  simulation: { COLOR_TRANSFORMATION_MATRICES },
     14 } = require("resource://devtools/server/actors/accessibility/constants.js");
     15 
     16 loader.lazyRequireGetter(
     17  this,
     18  "AccessibleActor",
     19  "resource://devtools/server/actors/accessibility/accessible.js",
     20  true
     21 );
     22 loader.lazyRequireGetter(
     23  this,
     24  ["CustomHighlighterActor"],
     25  "resource://devtools/server/actors/highlighters.js",
     26  true
     27 );
     28 loader.lazyRequireGetter(
     29  this,
     30  "DevToolsUtils",
     31  "resource://devtools/shared/DevToolsUtils.js"
     32 );
     33 loader.lazyRequireGetter(
     34  this,
     35  "events",
     36  "resource://devtools/shared/event-emitter.js"
     37 );
     38 loader.lazyRequireGetter(
     39  this,
     40  ["isWindowIncluded", "isFrameWithChildTarget"],
     41  "resource://devtools/shared/layout/utils.js",
     42  true
     43 );
     44 loader.lazyRequireGetter(
     45  this,
     46  "isXUL",
     47  "resource://devtools/server/actors/highlighters/utils/markup.js",
     48  true
     49 );
     50 loader.lazyRequireGetter(
     51  this,
     52  [
     53    "isDefunct",
     54    "loadSheetForBackgroundCalculation",
     55    "removeSheetForBackgroundCalculation",
     56  ],
     57  "resource://devtools/server/actors/utils/accessibility.js",
     58  true
     59 );
     60 loader.lazyRequireGetter(
     61  this,
     62  "accessibility",
     63  "resource://devtools/shared/constants.js",
     64  true
     65 );
     66 
     67 const lazy = {};
     68 ChromeUtils.defineESModuleGetters(
     69  lazy,
     70  {
     71    TYPES: "resource://devtools/shared/highlighters.mjs",
     72  },
     73  { global: "contextual" }
     74 );
     75 
     76 const kStateHover = 0x00000004; // ElementState::HOVER
     77 
     78 const {
     79  EVENT_TEXT_CHANGED,
     80  EVENT_TEXT_INSERTED,
     81  EVENT_TEXT_REMOVED,
     82  EVENT_ACCELERATOR_CHANGE,
     83  EVENT_ACTION_CHANGE,
     84  EVENT_DEFACTION_CHANGE,
     85  EVENT_DESCRIPTION_CHANGE,
     86  EVENT_DOCUMENT_ATTRIBUTES_CHANGED,
     87  EVENT_HIDE,
     88  EVENT_NAME_CHANGE,
     89  EVENT_OBJECT_ATTRIBUTE_CHANGED,
     90  EVENT_REORDER,
     91  EVENT_STATE_CHANGE,
     92  EVENT_TEXT_ATTRIBUTE_CHANGED,
     93  EVENT_VALUE_CHANGE,
     94 } = Ci.nsIAccessibleEvent;
     95 
     96 // TODO: We do not need this once bug 1422913 is fixed. We also would not need
     97 // to fire a name change event for an accessible that has an updated subtree and
     98 // that has its name calculated from the said subtree.
     99 const NAME_FROM_SUBTREE_RULE_ROLES = new Set([
    100  Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
    101  Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
    102  Ci.nsIAccessibleRole.ROLE_CELL,
    103  Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
    104  Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
    105  Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
    106  Ci.nsIAccessibleRole.ROLE_COLUMNHEADER,
    107  Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION,
    108  Ci.nsIAccessibleRole.ROLE_DEFINITION,
    109  Ci.nsIAccessibleRole.ROLE_GRID_CELL,
    110  Ci.nsIAccessibleRole.ROLE_HEADING,
    111  Ci.nsIAccessibleRole.ROLE_KEY,
    112  Ci.nsIAccessibleRole.ROLE_LABEL,
    113  Ci.nsIAccessibleRole.ROLE_LINK,
    114  Ci.nsIAccessibleRole.ROLE_LISTITEM,
    115  Ci.nsIAccessibleRole.ROLE_MATHML_IDENTIFIER,
    116  Ci.nsIAccessibleRole.ROLE_MATHML_NUMBER,
    117  Ci.nsIAccessibleRole.ROLE_MATHML_OPERATOR,
    118  Ci.nsIAccessibleRole.ROLE_MATHML_TEXT,
    119  Ci.nsIAccessibleRole.ROLE_MATHML_STRING_LITERAL,
    120  Ci.nsIAccessibleRole.ROLE_MATHML_GLYPH,
    121  Ci.nsIAccessibleRole.ROLE_MENUITEM,
    122  Ci.nsIAccessibleRole.ROLE_OPTION,
    123  Ci.nsIAccessibleRole.ROLE_OUTLINEITEM,
    124  Ci.nsIAccessibleRole.ROLE_PAGETAB,
    125  Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM,
    126  Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
    127  Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
    128  Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
    129  Ci.nsIAccessibleRole.ROLE_RICH_OPTION,
    130  Ci.nsIAccessibleRole.ROLE_ROW,
    131  Ci.nsIAccessibleRole.ROLE_ROWHEADER,
    132  Ci.nsIAccessibleRole.ROLE_SUMMARY,
    133  Ci.nsIAccessibleRole.ROLE_SWITCH,
    134  Ci.nsIAccessibleRole.ROLE_TERM,
    135  Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
    136  Ci.nsIAccessibleRole.ROLE_TOOLTIP,
    137 ]);
    138 
    139 const IS_OSX = Services.appinfo.OS === "Darwin";
    140 
    141 const {
    142  SCORES: { BEST_PRACTICES, FAIL, WARNING },
    143 } = accessibility;
    144 
    145 /**
    146 * Helper function that determines if nsIAccessible object is in stale state. When an
    147 * object is stale it means its subtree is not up to date.
    148 *
    149 * @param  {nsIAccessible}  accessible
    150 *         object to be tested.
    151 * @return {boolean}
    152 *         True if accessible object is stale, false otherwise.
    153 */
    154 function isStale(accessible) {
    155  const extraState = {};
    156  accessible.getState({}, extraState);
    157  // extraState.value is a bitmask. We are applying bitwise AND to mask out
    158  // irrelevant states.
    159  return !!(extraState.value & Ci.nsIAccessibleStates.EXT_STATE_STALE);
    160 }
    161 
    162 /**
    163 * Get accessibility audit starting with the passed accessible object as a root.
    164 *
    165 * @param {object} acc
    166 *        AccessibileActor to be used as the root for the audit.
    167 * @param {object} options
    168 *        Options for running audit, may include:
    169 *        - types: Array of audit types to be performed during audit.
    170 * @param {Map} report
    171 *        An accumulator map to be used to store audit information.
    172 * @param {object} progress
    173 *        An audit project object that is used to track the progress of the
    174 *        audit and send progress "audit-event" events to the client.
    175 */
    176 function getAudit(acc, options, report, progress) {
    177  if (acc.isDefunct) {
    178    return;
    179  }
    180 
    181  // Audit returns a promise, save the actual value in the report.
    182  report.set(
    183    acc,
    184    acc.audit(options).then(result => {
    185      report.set(acc, result);
    186      progress.increment();
    187    })
    188  );
    189 
    190  for (const child of acc.children()) {
    191    getAudit(child, options, report, progress);
    192  }
    193 }
    194 
    195 /**
    196 * A helper class that is used to track audit progress and send progress events
    197 * to the client.
    198 */
    199 class AuditProgress {
    200  constructor(walker) {
    201    this.completed = 0;
    202    this.percentage = 0;
    203    this.walker = walker;
    204  }
    205 
    206  setTotal(size) {
    207    this.size = size;
    208  }
    209 
    210  notify() {
    211    this.walker.emit("audit-event", {
    212      type: "progress",
    213      progress: {
    214        total: this.size,
    215        percentage: this.percentage,
    216        completed: this.completed,
    217      },
    218    });
    219  }
    220 
    221  increment() {
    222    this.completed++;
    223    const { completed, size } = this;
    224    if (!size) {
    225      return;
    226    }
    227 
    228    const percentage = Math.round((completed / size) * 100);
    229    if (percentage > this.percentage) {
    230      this.percentage = percentage;
    231      this.notify();
    232    }
    233  }
    234 
    235  destroy() {
    236    this.walker = null;
    237  }
    238 }
    239 
    240 /**
    241 * The AccessibleWalkerActor stores a cache of AccessibleActors that represent
    242 * accessible objects in a given document.
    243 *
    244 * It is also responsible for implicitely initializing and shutting down
    245 * accessibility engine by storing a reference to the XPCOM accessibility
    246 * service.
    247 */
    248 class AccessibleWalkerActor extends Actor {
    249  constructor(conn, targetActor) {
    250    super(conn, accessibleWalkerSpec);
    251    this.targetActor = targetActor;
    252    this.refMap = new Map();
    253    this._loadedSheets = new WeakMap();
    254    this.setA11yServiceGetter();
    255    this.onPick = this.onPick.bind(this);
    256    this.onHovered = this.onHovered.bind(this);
    257    this._preventContentEvent = this._preventContentEvent.bind(this);
    258    this.onKey = this.onKey.bind(this);
    259    this.onFocusIn = this.onFocusIn.bind(this);
    260    this.onFocusOut = this.onFocusOut.bind(this);
    261    this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
    262  }
    263 
    264  get highlighter() {
    265    if (!this._highlighter) {
    266      this._highlighter = new CustomHighlighterActor(
    267        this,
    268        lazy.TYPES.ACCESSIBLE
    269      );
    270 
    271      this.manage(this._highlighter);
    272      this._highlighter.on("highlighter-event", this.onHighlighterEvent);
    273    }
    274 
    275    return this._highlighter;
    276  }
    277 
    278  get tabbingOrderHighlighter() {
    279    if (!this._tabbingOrderHighlighter) {
    280      this._tabbingOrderHighlighter = new CustomHighlighterActor(
    281        this,
    282        lazy.TYPES.TABBING_ORDER
    283      );
    284 
    285      this.manage(this._tabbingOrderHighlighter);
    286    }
    287 
    288    return this._tabbingOrderHighlighter;
    289  }
    290 
    291  setA11yServiceGetter() {
    292    DevToolsUtils.defineLazyGetter(this, "a11yService", () => {
    293      Services.obs.addObserver(this, "accessible-event");
    294      return Cc["@mozilla.org/accessibilityService;1"].getService(
    295        Ci.nsIAccessibilityService
    296      );
    297    });
    298  }
    299 
    300  get rootWin() {
    301    return this.targetActor && this.targetActor.window;
    302  }
    303 
    304  get rootDoc() {
    305    return this.targetActor && this.targetActor.window.document;
    306  }
    307 
    308  get isXUL() {
    309    return isXUL(this.rootWin);
    310  }
    311 
    312  get colorMatrix() {
    313    if (!this.targetActor.docShell) {
    314      return null;
    315    }
    316 
    317    const colorMatrix = this.targetActor.docShell.getColorMatrix();
    318    if (
    319      colorMatrix.length === 0 ||
    320      colorMatrix === COLOR_TRANSFORMATION_MATRICES.NONE
    321    ) {
    322      return null;
    323    }
    324 
    325    return colorMatrix;
    326  }
    327 
    328  reset() {
    329    try {
    330      Services.obs.removeObserver(this, "accessible-event");
    331    } catch (e) {
    332      // Accessible event observer might not have been initialized if a11y
    333      // service was never used.
    334    }
    335 
    336    this.cancelPick();
    337 
    338    // Clean up accessible actors cache.
    339    this.clearRefs();
    340 
    341    this._childrenPromise = null;
    342    delete this.a11yService;
    343    this.setA11yServiceGetter();
    344  }
    345 
    346  /**
    347   * Remove existing cache (of accessible actors) from tree.
    348   */
    349  clearRefs() {
    350    for (const actor of this.refMap.values()) {
    351      actor.destroy();
    352    }
    353  }
    354 
    355  destroy() {
    356    super.destroy();
    357 
    358    this.reset();
    359 
    360    if (this._highlighter) {
    361      this._highlighter.off("highlighter-event", this.onHighlighterEvent);
    362      this._highlighter = null;
    363    }
    364 
    365    if (this._tabbingOrderHighlighter) {
    366      this._tabbingOrderHighlighter = null;
    367    }
    368 
    369    this.targetActor = null;
    370    this.refMap = null;
    371  }
    372 
    373  getRef(rawAccessible) {
    374    return this.refMap.get(rawAccessible);
    375  }
    376 
    377  addRef(rawAccessible) {
    378    let actor = this.refMap.get(rawAccessible);
    379    if (actor) {
    380      return actor;
    381    }
    382 
    383    actor = new AccessibleActor(this, rawAccessible);
    384    // Add the accessible actor as a child of this accessible walker actor,
    385    // assigning it an actorID.
    386    this.manage(actor);
    387    this.refMap.set(rawAccessible, actor);
    388 
    389    return actor;
    390  }
    391 
    392  /**
    393   * Clean up accessible actors cache for a given accessible's subtree.
    394   *
    395   * @param  {null|nsIAccessible} rawAccessible
    396   */
    397  purgeSubtree(rawAccessible) {
    398    if (!rawAccessible) {
    399      return;
    400    }
    401 
    402    try {
    403      for (
    404        let child = rawAccessible.firstChild;
    405        child;
    406        child = child.nextSibling
    407      ) {
    408        this.purgeSubtree(child);
    409      }
    410    } catch (e) {
    411      // rawAccessible or its descendants are defunct.
    412    }
    413 
    414    const actor = this.getRef(rawAccessible);
    415    if (actor) {
    416      actor.destroy();
    417    }
    418  }
    419 
    420  unmanage(actor) {
    421    if (actor instanceof AccessibleActor) {
    422      this.refMap.delete(actor.rawAccessible);
    423    }
    424    Actor.prototype.unmanage.call(this, actor);
    425  }
    426 
    427  /**
    428   * A helper method. Accessibility walker is assumed to have only 1 child which
    429   * is the top level document.
    430   */
    431  async children() {
    432    if (this._childrenPromise) {
    433      return this._childrenPromise;
    434    }
    435 
    436    this._childrenPromise = Promise.all([this.getDocument()]);
    437    const children = await this._childrenPromise;
    438    this._childrenPromise = null;
    439    return children;
    440  }
    441 
    442  /**
    443   * A promise for a root document accessible actor that only resolves when its
    444   * corresponding document accessible object is fully loaded.
    445   *
    446   * @return {Promise}
    447   */
    448  getDocument() {
    449    if (!this.rootDoc || !this.rootDoc.documentElement) {
    450      return this.once("document-ready").then(docAcc => this.addRef(docAcc));
    451    }
    452 
    453    if (this.isXUL) {
    454      const doc = this.addRef(this.getRawAccessibleFor(this.rootDoc));
    455      return Promise.resolve(doc);
    456    }
    457 
    458    const doc = this.getRawAccessibleFor(this.rootDoc);
    459 
    460    // For non-visible same-process iframes we don't get a document and
    461    // won't get a "document-ready" event.
    462    if (!doc && !this.rootWin.windowGlobalChild.isProcessRoot) {
    463      // We can ignore such document as there won't be anything to audit in them.
    464      return null;
    465    }
    466 
    467    if (!doc || isStale(doc)) {
    468      return this.once("document-ready").then(docAcc => this.addRef(docAcc));
    469    }
    470 
    471    return Promise.resolve(this.addRef(doc));
    472  }
    473 
    474  /**
    475   * Get an accessible actor for a domnode actor.
    476   *
    477   * @param  {object} domNode
    478   *         domnode actor for which accessible actor is being created.
    479   * @return {Promse}
    480   *         A promise that resolves when accessible actor is created for a
    481   *         domnode actor.
    482   */
    483  getAccessibleFor(domNode) {
    484    // We need to make sure that the document is loaded processed by a11y first.
    485    return this.getDocument().then(() => {
    486      const rawAccessible = this.getRawAccessibleFor(domNode.rawNode);
    487      // Not all DOM nodes have corresponding accessible objects. It's usually
    488      // the case where there is no semantics or relevance to the accessibility
    489      // client.
    490      if (!rawAccessible) {
    491        return null;
    492      }
    493 
    494      return this.addRef(rawAccessible);
    495    });
    496  }
    497 
    498  /**
    499   * Get a raw accessible object for a raw node.
    500   *
    501   * @param  {DOMNode} rawNode
    502   *         Raw node for which accessible object is being retrieved.
    503   * @return {nsIAccessible}
    504   *         Accessible object for a given DOMNode.
    505   */
    506  getRawAccessibleFor(rawNode) {
    507    // Accessible can only be retrieved iff accessibility service is enabled.
    508    if (!Services.appinfo.accessibilityEnabled) {
    509      return null;
    510    }
    511 
    512    return this.a11yService.getAccessibleFor(rawNode);
    513  }
    514 
    515  async getAncestry(accessible) {
    516    if (!accessible || accessible.indexInParent === -1) {
    517      return [];
    518    }
    519    const doc = await this.getDocument();
    520    if (!doc) {
    521      return [];
    522    }
    523 
    524    const ancestry = [];
    525    if (accessible === doc) {
    526      return ancestry;
    527    }
    528 
    529    try {
    530      let parent = accessible;
    531      while (parent && (parent = parent.parentAcc) && parent != doc) {
    532        ancestry.push(parent);
    533      }
    534      ancestry.push(doc);
    535    } catch (error) {
    536      throw new Error(`Failed to get ancestor for ${accessible}: ${error}`);
    537    }
    538 
    539    return ancestry.map(parent => ({
    540      accessible: parent,
    541      children: parent.children(),
    542    }));
    543  }
    544 
    545  /**
    546   * Run accessibility audit and return relevant ancestries for AccessibleActors
    547   * that have non-empty audit checks.
    548   *
    549   * @param  {object} options
    550   *         Options for running audit, may include:
    551   *         - types: Array of audit types to be performed during audit.
    552   *
    553   * @return {Promise}
    554   *         A promise that resolves when the audit is complete and all relevant
    555   *         ancestries are calculated.
    556   */
    557  async audit(options) {
    558    const doc = await this.getDocument();
    559    if (!doc) {
    560      return [];
    561    }
    562 
    563    const report = new Map();
    564    this._auditProgress = new AuditProgress(this);
    565    getAudit(doc, options, report, this._auditProgress);
    566    this._auditProgress.setTotal(report.size);
    567    await Promise.all(report.values());
    568 
    569    const ancestries = [];
    570    for (const [acc, audit] of report.entries()) {
    571      // Filter out audits that have no failing checks.
    572      if (
    573        audit &&
    574        Object.values(audit).some(
    575          check =>
    576            check != null &&
    577            !check.error &&
    578            [BEST_PRACTICES, FAIL, WARNING].includes(check.score)
    579        )
    580      ) {
    581        ancestries.push(this.getAncestry(acc));
    582      }
    583    }
    584 
    585    return Promise.all(ancestries);
    586  }
    587 
    588  /**
    589   * Start accessibility audit. The result of this function will not be an audit
    590   * report. Instead, an "audit-event" event will be fired when the audit is
    591   * completed or fails.
    592   *
    593   * @param {object} options
    594   *        Options for running audit, may include:
    595   *        - types: Array of audit types to be performed during audit.
    596   */
    597  startAudit(options) {
    598    // Audit is already running, wait for the "audit-event" event.
    599    if (this._auditing) {
    600      return;
    601    }
    602 
    603    this._auditing = this.audit(options)
    604      // We do not want to block on audit request, instead fire "audit-event"
    605      // event when internal audit is finished or failed.
    606      .then(ancestries =>
    607        this.emit("audit-event", {
    608          type: "completed",
    609          ancestries,
    610        })
    611      )
    612      .catch(() => this.emit("audit-event", { type: "error" }))
    613      .finally(() => {
    614        this._auditing = null;
    615        if (this._auditProgress) {
    616          this._auditProgress.destroy();
    617          this._auditProgress = null;
    618        }
    619      });
    620  }
    621 
    622  onHighlighterEvent(data) {
    623    this.emit("highlighter-event", data);
    624  }
    625 
    626  /**
    627   * Accessible event observer function.
    628   *
    629   * @param {Ci.nsIAccessibleEvent} subject
    630   *                                      accessible event object.
    631   */
    632  // eslint-disable-next-line complexity
    633  observe(subject) {
    634    const event = subject.QueryInterface(Ci.nsIAccessibleEvent);
    635    const rawAccessible = event.accessible;
    636    const accessible = this.getRef(rawAccessible);
    637 
    638    if (rawAccessible instanceof Ci.nsIAccessibleDocument && !accessible) {
    639      const rootDocAcc = this.getRawAccessibleFor(this.rootDoc);
    640      if (rawAccessible === rootDocAcc && !isStale(rawAccessible)) {
    641        this.clearRefs();
    642        // If it's a top level document notify listeners about the document
    643        // being ready.
    644        this.emit("document-ready", rawAccessible);
    645      }
    646    }
    647 
    648    switch (event.eventType) {
    649      case EVENT_STATE_CHANGE: {
    650        const { state, isEnabled } = event.QueryInterface(
    651          Ci.nsIAccessibleStateChangeEvent
    652        );
    653        const isBusy = state & Ci.nsIAccessibleStates.STATE_BUSY;
    654        if (accessible) {
    655          // Only propagate state change events for active accessibles.
    656          if (isBusy && isEnabled) {
    657            if (rawAccessible instanceof Ci.nsIAccessibleDocument) {
    658              // Remove existing cache from tree.
    659              this.clearRefs();
    660            }
    661            return;
    662          }
    663          accessible.emit("states-change", accessible.states);
    664        }
    665 
    666        break;
    667      }
    668      case EVENT_NAME_CHANGE:
    669        if (accessible) {
    670          accessible.emit(
    671            "name-change",
    672            rawAccessible.name,
    673            event.DOMNode == this.rootDoc
    674              ? undefined
    675              : this.getRef(rawAccessible.parent)
    676          );
    677        }
    678        break;
    679      case EVENT_VALUE_CHANGE:
    680        if (accessible) {
    681          accessible.emit("value-change", rawAccessible.value);
    682        }
    683        break;
    684      case EVENT_DESCRIPTION_CHANGE:
    685        if (accessible) {
    686          accessible.emit("description-change", rawAccessible.description);
    687        }
    688        break;
    689      case EVENT_REORDER:
    690        if (accessible) {
    691          accessible
    692            .children()
    693            .forEach(child =>
    694              child.emit("index-in-parent-change", child.indexInParent)
    695            );
    696          accessible.emit("reorder", rawAccessible.childCount);
    697        }
    698        break;
    699      case EVENT_HIDE:
    700        if (event.DOMNode == this.rootDoc) {
    701          this.clearRefs();
    702        } else {
    703          this.purgeSubtree(rawAccessible);
    704        }
    705        break;
    706      case EVENT_DEFACTION_CHANGE:
    707      case EVENT_ACTION_CHANGE:
    708        if (accessible) {
    709          accessible.emit("actions-change", accessible.actions);
    710        }
    711        break;
    712      case EVENT_TEXT_CHANGED:
    713      case EVENT_TEXT_INSERTED:
    714      case EVENT_TEXT_REMOVED:
    715        if (accessible) {
    716          accessible.emit("text-change");
    717          if (NAME_FROM_SUBTREE_RULE_ROLES.has(rawAccessible.role)) {
    718            accessible.emit(
    719              "name-change",
    720              rawAccessible.name,
    721              event.DOMNode == this.rootDoc
    722                ? undefined
    723                : this.getRef(rawAccessible.parent)
    724            );
    725          }
    726        }
    727        break;
    728      case EVENT_DOCUMENT_ATTRIBUTES_CHANGED:
    729      case EVENT_OBJECT_ATTRIBUTE_CHANGED:
    730      case EVENT_TEXT_ATTRIBUTE_CHANGED:
    731        if (accessible) {
    732          accessible.emit("attributes-change", accessible.attributes);
    733        }
    734        break;
    735      // EVENT_ACCELERATOR_CHANGE is currently not fired by gecko accessibility.
    736      case EVENT_ACCELERATOR_CHANGE:
    737        if (accessible) {
    738          accessible.emit("shortcut-change", accessible.keyboardShortcut);
    739        }
    740        break;
    741      default:
    742        break;
    743    }
    744  }
    745 
    746  /**
    747   * Ensure that nothing interferes with the audit for an accessible object
    748   * (CSS, overlays) by load accessibility highlighter style sheet used for
    749   * preventing transitions and applying transparency when calculating colour
    750   * contrast as well as temporarily hiding accessible highlighter overlay.
    751   *
    752   * @param  {object} win
    753   *         Window where highlighting happens.
    754   */
    755  async clearStyles(win) {
    756    const requests = this._loadedSheets.get(win);
    757    if (requests != null) {
    758      this._loadedSheets.set(win, requests + 1);
    759      return;
    760    }
    761 
    762    // Disable potential mouse driven transitions (This is important because accessibility
    763    // highlighter temporarily modifies text color related CSS properties. In case where
    764    // there are transitions that affect them, there might be unexpected side effects when
    765    // taking a snapshot for contrast measurement).
    766    loadSheetForBackgroundCalculation(win);
    767    this._loadedSheets.set(win, 1);
    768    await this.hideHighlighter();
    769  }
    770 
    771  /**
    772   * Restore CSS and overlays that could've interfered with the audit for an
    773   * accessible object by unloading accessibility highlighter style sheet used
    774   * for preventing transitions and applying transparency when calculating
    775   * colour contrast and potentially restoring accessible highlighter overlay.
    776   *
    777   * @param  {object} win
    778   *         Window where highlighting was happenning.
    779   */
    780  async restoreStyles(win) {
    781    const requests = this._loadedSheets.get(win);
    782    if (!requests) {
    783      return;
    784    }
    785 
    786    if (requests > 1) {
    787      this._loadedSheets.set(win, requests - 1);
    788      return;
    789    }
    790 
    791    await this.showHighlighter();
    792    removeSheetForBackgroundCalculation(win);
    793    this._loadedSheets.delete(win);
    794  }
    795 
    796  async hideHighlighter() {
    797    // TODO: Fix this workaround that temporarily removes higlighter bounds
    798    // overlay that can interfere with the contrast ratio calculation.
    799    if (this._highlighter) {
    800      const highlighter = this._highlighter.instance;
    801      await highlighter.isReady;
    802      highlighter.hideAccessibleBounds();
    803    }
    804  }
    805 
    806  async showHighlighter() {
    807    // TODO: Fix this workaround that temporarily removes higlighter bounds
    808    // overlay that can interfere with the contrast ratio calculation.
    809    if (this._highlighter) {
    810      const highlighter = this._highlighter.instance;
    811      await highlighter.isReady;
    812      highlighter.showAccessibleBounds();
    813    }
    814  }
    815 
    816  /**
    817   * Public method used to show an accessible object highlighter on the client
    818   * side.
    819   *
    820   * @param  {object} accessible
    821   *         AccessibleActor to be highlighted.
    822   * @param  {object} options
    823   *         Object used for passing options. Available options:
    824   *         - duration {Number}
    825   *                    Duration of time that the highlighter should be shown.
    826   * @return {boolean}
    827   *         True if highlighter shows the accessible object.
    828   */
    829  async highlightAccessible(accessible, options = {}) {
    830    this.unhighlight();
    831    // Do not highlight if accessible is dead.
    832    if (!accessible || accessible.isDefunct || accessible.indexInParent < 0) {
    833      return false;
    834    }
    835 
    836    this._highlightingAccessible = accessible;
    837    const { bounds } = accessible;
    838    if (!bounds) {
    839      return false;
    840    }
    841 
    842    const { DOMNode: rawNode } = accessible.rawAccessible;
    843    const audit = await accessible.audit();
    844    if (this._highlightingAccessible !== accessible) {
    845      return false;
    846    }
    847 
    848    const { name, role } = accessible;
    849    const { highlighter } = this;
    850    await highlighter.instance.isReady;
    851    if (this._highlightingAccessible !== accessible) {
    852      return false;
    853    }
    854 
    855    const shown = highlighter.show(
    856      { rawNode },
    857      { ...options, ...bounds, name, role, audit, isXUL: this.isXUL }
    858    );
    859    this._highlightingAccessible = null;
    860 
    861    return shown;
    862  }
    863 
    864  /**
    865   * Public method used to hide an accessible object highlighter on the client
    866   * side.
    867   */
    868  unhighlight() {
    869    if (!this._highlighter) {
    870      return;
    871    }
    872 
    873    this.highlighter.hide();
    874    this._highlightingAccessible = null;
    875  }
    876 
    877  /**
    878   * Picking state that indicates if picking is currently enabled and, if so,
    879   * what the current and hovered accessible objects are.
    880   */
    881  _isPicking = false;
    882  _currentAccessible = null;
    883 
    884  /**
    885   * Check is event handling is allowed.
    886   */
    887  _isEventAllowed({ view }) {
    888    return this.rootWin.isChromeWindow || isWindowIncluded(this.rootWin, view);
    889  }
    890 
    891  /**
    892   * Check if the DOM event received when picking shold be ignored.
    893   *
    894   * @param {Event} event
    895   */
    896  _ignoreEventWhenPicking(event) {
    897    return (
    898      !this._isPicking ||
    899      // If the DOM event is about a remote frame, only the WalkerActor for that
    900      // remote frame target should emit RDP events (hovered/picked/...). And
    901      // all other WalkerActor for intermediate iframe and top level document
    902      // targets should stay silent.
    903      isFrameWithChildTarget(
    904        this.targetActor,
    905        event.originalTarget || event.target
    906      )
    907    );
    908  }
    909 
    910  _preventContentEvent(event) {
    911    if (this._ignoreEventWhenPicking(event)) {
    912      return;
    913    }
    914 
    915    event.stopPropagation();
    916    event.preventDefault();
    917 
    918    const target = event.originalTarget || event.target;
    919    if (target !== this._currentTarget) {
    920      this._resetStateAndReleaseTarget();
    921      this._currentTarget = target;
    922      // We use InspectorUtils to save the original hover content state of the target
    923      // element (that includes its hover state). In order to not trigger any visual
    924      // changes to the element that depend on its hover state we remove the state while
    925      // the element is the most current target of the highlighter.
    926      //
    927      // TODO: This logic can be removed if/when we can use elementsAtPoint API for
    928      // determining topmost DOMNode that corresponds to specific coordinates. We would
    929      // then be able to use a highlighter overlay that would prevent all pointer events
    930      // to content but still render highlighter for the node/element correctly.
    931      this._currentTargetHoverState =
    932        InspectorUtils.getContentState(target) & kStateHover;
    933      InspectorUtils.removeContentState(target, kStateHover);
    934    }
    935  }
    936 
    937  /**
    938   * Click event handler for when picking is enabled.
    939   *
    940   * @param  {object} event
    941   *         Current click event.
    942   */
    943  onPick(event) {
    944    if (this._ignoreEventWhenPicking(event)) {
    945      return;
    946    }
    947 
    948    this._preventContentEvent(event);
    949    if (!this._isEventAllowed(event)) {
    950      return;
    951    }
    952 
    953    // If shift is pressed, this is only a preview click, send the event to
    954    // the client, but don't stop picking.
    955    if (event.shiftKey) {
    956      if (!this._currentAccessible) {
    957        this._currentAccessible = this._findAndAttachAccessible(event);
    958      }
    959      this.emit("picker-accessible-previewed", this._currentAccessible);
    960      return;
    961    }
    962 
    963    this._unsetPickerEnvironment();
    964    this._isPicking = false;
    965    if (!this._currentAccessible) {
    966      this._currentAccessible = this._findAndAttachAccessible(event);
    967    }
    968    this.emit("picker-accessible-picked", this._currentAccessible);
    969  }
    970 
    971  /**
    972   * Hover event handler for when picking is enabled.
    973   *
    974   * @param  {object} event
    975   *         Current hover event.
    976   */
    977  async onHovered(event) {
    978    if (this._ignoreEventWhenPicking(event)) {
    979      return;
    980    }
    981 
    982    this._preventContentEvent(event);
    983    if (!this._isEventAllowed(event)) {
    984      return;
    985    }
    986 
    987    const accessible = this._findAndAttachAccessible(event);
    988    if (!accessible || this._currentAccessible === accessible) {
    989      return;
    990    }
    991 
    992    this._currentAccessible = accessible;
    993    // Highlight current accessible and by the time we are done, if accessible that was
    994    // highlighted is not current any more (user moved the mouse to a new node) highlight
    995    // the most current accessible again.
    996    const shown = await this.highlightAccessible(accessible);
    997    if (this._isPicking && shown && accessible === this._currentAccessible) {
    998      this.emit("picker-accessible-hovered", accessible);
    999    }
   1000  }
   1001 
   1002  /**
   1003   * Keyboard event handler for when picking is enabled.
   1004   *
   1005   * @param  {object} event
   1006   *         Current keyboard event.
   1007   */
   1008  onKey(event) {
   1009    if (!this._currentAccessible || this._ignoreEventWhenPicking(event)) {
   1010      return;
   1011    }
   1012 
   1013    this._preventContentEvent(event);
   1014    if (!this._isEventAllowed(event)) {
   1015      return;
   1016    }
   1017 
   1018    /**
   1019     * KEY: Action/scope
   1020     * ENTER/CARRIAGE_RETURN: Picks current accessible
   1021     * ESC/CTRL+SHIFT+C: Cancels picker
   1022     */
   1023    switch (event.keyCode) {
   1024      // Select the element.
   1025      case event.DOM_VK_RETURN:
   1026        this.onPick(event);
   1027        break;
   1028      // Cancel pick mode.
   1029      case event.DOM_VK_ESCAPE:
   1030        this.cancelPick();
   1031        this.emit("picker-accessible-canceled");
   1032        break;
   1033      case event.DOM_VK_C:
   1034        if (
   1035          (IS_OSX && event.metaKey && event.altKey) ||
   1036          (!IS_OSX && event.ctrlKey && event.shiftKey)
   1037        ) {
   1038          this.cancelPick();
   1039          this.emit("picker-accessible-canceled");
   1040        }
   1041        break;
   1042      default:
   1043        break;
   1044    }
   1045  }
   1046 
   1047  /**
   1048   * Picker method that starts picker content listeners.
   1049   */
   1050  pick() {
   1051    if (!this._isPicking) {
   1052      this._isPicking = true;
   1053      this._setPickerEnvironment();
   1054    }
   1055  }
   1056 
   1057  /**
   1058   * This pick method also focuses the highlighter's target window.
   1059   */
   1060  pickAndFocus() {
   1061    this.pick();
   1062    this.rootWin.focus();
   1063  }
   1064 
   1065  attachAccessible(rawAccessible, accessibleDocument) {
   1066    // If raw accessible object is defunct or detached, no need to cache it and
   1067    // its ancestry.
   1068    if (
   1069      !rawAccessible ||
   1070      isDefunct(rawAccessible) ||
   1071      rawAccessible.indexInParent < 0
   1072    ) {
   1073      return null;
   1074    }
   1075 
   1076    const accessible = this.addRef(rawAccessible);
   1077    // There is a chance that ancestry lookup can fail if the accessible is in
   1078    // the detached subtree. At that point the root accessible object would be
   1079    // defunct and accessing it via parent property will throw.
   1080    try {
   1081      let parent = accessible;
   1082      while (parent && parent.rawAccessible != accessibleDocument) {
   1083        parent = parent.parentAcc;
   1084      }
   1085    } catch (error) {
   1086      throw new Error(`Failed to get ancestor for ${accessible}: ${error}`);
   1087    }
   1088 
   1089    return accessible;
   1090  }
   1091 
   1092  /**
   1093   * Find deepest accessible object that corresponds to the screen coordinates of the
   1094   * mouse pointer and attach it to the AccessibilityWalker tree.
   1095   *
   1096   * @param  {object} event
   1097   *         Correspoinding content event.
   1098   * @return {null | object}
   1099   *         Accessible object, if available, that corresponds to a DOM node.
   1100   */
   1101  _findAndAttachAccessible(event) {
   1102    const target = event.originalTarget || event.target;
   1103    const win = target.ownerGlobal;
   1104    // This event might be inside a sub-document, so don't use this.rootDoc.
   1105    const docAcc = this.getRawAccessibleFor(win.document);
   1106    // If the target is inside a pop-up widget, we need to query the pop-up
   1107    // Accessible, not the DocAccessible. The DocAccessible can't hit test
   1108    // inside pop-ups.
   1109    const popup = win.isChromeWindow
   1110      ? target.closest("panel, menupopup")
   1111      : null;
   1112    const containerAcc = popup ? this.getRawAccessibleFor(popup) : docAcc;
   1113    const { devicePixelRatio } = this.rootWin;
   1114    const rawAccessible = containerAcc.getDeepestChildAtPointInProcess(
   1115      event.screenX * devicePixelRatio,
   1116      event.screenY * devicePixelRatio
   1117    );
   1118    return this.attachAccessible(rawAccessible, docAcc);
   1119  }
   1120 
   1121  /**
   1122   * Start picker content listeners.
   1123   */
   1124  _setPickerEnvironment() {
   1125    const target = this.targetActor.chromeEventHandler;
   1126    target.addEventListener("mousemove", this.onHovered, true);
   1127    target.addEventListener("click", this.onPick, true);
   1128    target.addEventListener("mousedown", this._preventContentEvent, true);
   1129    target.addEventListener("mouseup", this._preventContentEvent, true);
   1130    target.addEventListener("mouseover", this._preventContentEvent, true);
   1131    target.addEventListener("mouseout", this._preventContentEvent, true);
   1132    target.addEventListener("mouseleave", this._preventContentEvent, true);
   1133    target.addEventListener("mouseenter", this._preventContentEvent, true);
   1134    target.addEventListener("dblclick", this._preventContentEvent, true);
   1135    target.addEventListener("keydown", this.onKey, true);
   1136    target.addEventListener("keyup", this._preventContentEvent, true);
   1137  }
   1138 
   1139  /**
   1140   * If content is still alive, stop picker content listeners, reset the hover state for
   1141   * last target element.
   1142   */
   1143  _unsetPickerEnvironment() {
   1144    const target = this.targetActor.chromeEventHandler;
   1145 
   1146    if (!target) {
   1147      return;
   1148    }
   1149 
   1150    target.removeEventListener("mousemove", this.onHovered, true);
   1151    target.removeEventListener("click", this.onPick, true);
   1152    target.removeEventListener("mousedown", this._preventContentEvent, true);
   1153    target.removeEventListener("mouseup", this._preventContentEvent, true);
   1154    target.removeEventListener("mouseover", this._preventContentEvent, true);
   1155    target.removeEventListener("mouseout", this._preventContentEvent, true);
   1156    target.removeEventListener("mouseleave", this._preventContentEvent, true);
   1157    target.removeEventListener("mouseenter", this._preventContentEvent, true);
   1158    target.removeEventListener("dblclick", this._preventContentEvent, true);
   1159    target.removeEventListener("keydown", this.onKey, true);
   1160    target.removeEventListener("keyup", this._preventContentEvent, true);
   1161 
   1162    this._resetStateAndReleaseTarget();
   1163  }
   1164 
   1165  /**
   1166   * When using accessibility highlighter, we keep track of the most current event pointer
   1167   * event target. In order to update or release the target, we need to make sure we set
   1168   * the content state (using InspectorUtils) to its original value.
   1169   *
   1170   * TODO: This logic can be removed if/when we can use elementsAtPoint API for
   1171   * determining topmost DOMNode that corresponds to specific coordinates. We would then
   1172   * be able to use a highlighter overlay that would prevent all pointer events to content
   1173   * but still render highlighter for the node/element correctly.
   1174   */
   1175  _resetStateAndReleaseTarget() {
   1176    if (!this._currentTarget) {
   1177      return;
   1178    }
   1179 
   1180    try {
   1181      if (this._currentTargetHoverState) {
   1182        InspectorUtils.setContentState(this._currentTarget, kStateHover);
   1183      }
   1184    } catch (e) {
   1185      // DOMNode is already dead.
   1186    }
   1187 
   1188    this._currentTarget = null;
   1189    this._currentTargetState = null;
   1190  }
   1191 
   1192  /**
   1193   * Cacncel picker pick. Remvoe all content listeners and hide the highlighter.
   1194   */
   1195  cancelPick() {
   1196    this.unhighlight();
   1197 
   1198    if (this._isPicking) {
   1199      this._unsetPickerEnvironment();
   1200      this._isPicking = false;
   1201      this._currentAccessible = null;
   1202    }
   1203  }
   1204 
   1205  /**
   1206   * Indicates that the tabbing order current active element (focused) is being
   1207   * tracked.
   1208   */
   1209  _isTrackingTabbingOrderFocus = false;
   1210 
   1211  /**
   1212   * Current focused element in the tabbing order.
   1213   */
   1214  _currentFocusedTabbingOrder = null;
   1215 
   1216  /**
   1217   * Focusin event handler for when interacting with tabbing order overlay.
   1218   *
   1219   * @param  {object} event
   1220   *         Most recent focusin event.
   1221   */
   1222  async onFocusIn(event) {
   1223    if (!this._isTrackingTabbingOrderFocus) {
   1224      return;
   1225    }
   1226 
   1227    const target = event.originalTarget || event.target;
   1228    if (target === this._currentFocusedTabbingOrder) {
   1229      return;
   1230    }
   1231 
   1232    this._currentFocusedTabbingOrder = target;
   1233    this.tabbingOrderHighlighter._highlighter.updateFocus({
   1234      node: target,
   1235      focused: true,
   1236    });
   1237  }
   1238 
   1239  /**
   1240   * Focusout event handler for when interacting with tabbing order overlay.
   1241   *
   1242   * @param  {object} event
   1243   *         Most recent focusout event.
   1244   */
   1245  async onFocusOut(event) {
   1246    if (
   1247      !this._isTrackingTabbingOrderFocus ||
   1248      !this._currentFocusedTabbingOrder
   1249    ) {
   1250      return;
   1251    }
   1252 
   1253    const target = event.originalTarget || event.target;
   1254    // Sanity check.
   1255    if (target !== this._currentFocusedTabbingOrder) {
   1256      console.warn(
   1257        `focusout target: ${target} does not match current focused element in tabbing order: ${this._currentFocusedTabbingOrder}`
   1258      );
   1259    }
   1260 
   1261    this.tabbingOrderHighlighter._highlighter.updateFocus({
   1262      node: this._currentFocusedTabbingOrder,
   1263      focused: false,
   1264    });
   1265    this._currentFocusedTabbingOrder = null;
   1266  }
   1267 
   1268  /**
   1269   * Show tabbing order overlay for a given target.
   1270   *
   1271   * @param  {object} elm
   1272   *         domnode actor to be used as the starting point for generating the
   1273   *         tabbing order.
   1274   * @param  {number} index
   1275   *         Starting index for the tabbing order.
   1276   *
   1277   * @return {JSON}
   1278   *         Tabbing order information for the last element in the tabbing
   1279   *         order. It includes a ContentDOMReference for the node and a tabbing
   1280   *         index. If we are at the end of the tabbing order for the top level
   1281   *         content document, the ContentDOMReference will be null. If focus
   1282   *         manager discovered a remote IFRAME, then the ContentDOMReference
   1283   *         references the IFRAME itself.
   1284   */
   1285  showTabbingOrder(elm, index) {
   1286    // Start track focus related events (only once). `showTabbingOrder` will be
   1287    // called multiple times for a given target if it contains other remote
   1288    // targets.
   1289    if (!this._isTrackingTabbingOrderFocus) {
   1290      this._isTrackingTabbingOrderFocus = true;
   1291      const target = this.targetActor.chromeEventHandler;
   1292      target.addEventListener("focusin", this.onFocusIn, true);
   1293      target.addEventListener("focusout", this.onFocusOut, true);
   1294    }
   1295 
   1296    return this.tabbingOrderHighlighter.show(elm, { index });
   1297  }
   1298 
   1299  /**
   1300   * Hide tabbing order overlay for a given target.
   1301   */
   1302  hideTabbingOrder() {
   1303    if (!this._tabbingOrderHighlighter) {
   1304      return;
   1305    }
   1306 
   1307    this.tabbingOrderHighlighter.hide();
   1308    if (!this._isTrackingTabbingOrderFocus) {
   1309      return;
   1310    }
   1311 
   1312    this._isTrackingTabbingOrderFocus = false;
   1313    this._currentFocusedTabbingOrder = null;
   1314    const target = this.targetActor.chromeEventHandler;
   1315    if (target) {
   1316      target.removeEventListener("focusin", this.onFocusIn, true);
   1317      target.removeEventListener("focusout", this.onFocusOut, true);
   1318    }
   1319  }
   1320 }
   1321 
   1322 exports.AccessibleWalkerActor = AccessibleWalkerActor;