Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  walker.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

const { Actor } = require("resource://devtools/shared/protocol.js");
const {
  accessibleWalkerSpec,
} = require("resource://devtools/shared/specs/accessibility.js");

const {
  simulation: { COLOR_TRANSFORMATION_MATRICES },
} = require("resource://devtools/server/actors/accessibility/constants.js");

loader.lazyRequireGetter(
  this,
  "AccessibleActor",
  "resource://devtools/server/actors/accessibility/accessible.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["CustomHighlighterActor"],
  "resource://devtools/server/actors/highlighters.js",
  true
);
loader.lazyRequireGetter(
  this,
  "DevToolsUtils",
  "resource://devtools/shared/DevToolsUtils.js"
);
loader.lazyRequireGetter(
  this,
  "events",
  "resource://devtools/shared/event-emitter.js"
);
loader.lazyRequireGetter(
  this,
  ["isWindowIncluded""isFrameWithChildTarget"],
  "resource://devtools/shared/layout/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  "isXUL",
  "resource://devtools/server/actors/highlighters/utils/markup.js",
  true
);
loader.lazyRequireGetter(
  this,
  [
    "isDefunct",
    "loadSheetForBackgroundCalculation",
    "removeSheetForBackgroundCalculation",
  ],
  "resource://devtools/server/actors/utils/accessibility.js",
  true
);
loader.lazyRequireGetter(
  this,
  "accessibility",
  "resource://devtools/shared/constants.js",
  true
);

const lazy = {};
ChromeUtils.defineESModuleGetters(
  lazy,
  {
    TYPES: "resource://devtools/shared/highlighters.mjs",
  },
  { global: "contextual" }
);

const kStateHover = 0x00000004; // ElementState::HOVER

const {
  EVENT_TEXT_CHANGED,
  EVENT_TEXT_INSERTED,
  EVENT_TEXT_REMOVED,
  EVENT_ACCELERATOR_CHANGE,
  EVENT_ACTION_CHANGE,
  EVENT_DEFACTION_CHANGE,
  EVENT_DESCRIPTION_CHANGE,
  EVENT_DOCUMENT_ATTRIBUTES_CHANGED,
  EVENT_HIDE,
  EVENT_NAME_CHANGE,
  EVENT_OBJECT_ATTRIBUTE_CHANGED,
  EVENT_REORDER,
  EVENT_STATE_CHANGE,
  EVENT_TEXT_ATTRIBUTE_CHANGED,
  EVENT_VALUE_CHANGE,
} = Ci.nsIAccessibleEvent;

// TODO: We do not need this once bug 1422913 is fixed. We also would not need
// to fire a name change event for an accessible that has an updated subtree and
// that has its name calculated from the said subtree.
const NAME_FROM_SUBTREE_RULE_ROLES = new Set([
  Ci.nsIAccessibleRole.ROLE_BUTTONDROPDOWN,
  Ci.nsIAccessibleRole.ROLE_BUTTONMENU,
  Ci.nsIAccessibleRole.ROLE_CELL,
  Ci.nsIAccessibleRole.ROLE_CHECKBUTTON,
  Ci.nsIAccessibleRole.ROLE_CHECK_MENU_ITEM,
  Ci.nsIAccessibleRole.ROLE_CHECK_RICH_OPTION,
  Ci.nsIAccessibleRole.ROLE_COLUMNHEADER,
  Ci.nsIAccessibleRole.ROLE_COMBOBOX_OPTION,
  Ci.nsIAccessibleRole.ROLE_DEFINITION,
  Ci.nsIAccessibleRole.ROLE_GRID_CELL,
  Ci.nsIAccessibleRole.ROLE_HEADING,
  Ci.nsIAccessibleRole.ROLE_KEY,
  Ci.nsIAccessibleRole.ROLE_LABEL,
  Ci.nsIAccessibleRole.ROLE_LINK,
  Ci.nsIAccessibleRole.ROLE_LISTITEM,
  Ci.nsIAccessibleRole.ROLE_MATHML_IDENTIFIER,
  Ci.nsIAccessibleRole.ROLE_MATHML_NUMBER,
  Ci.nsIAccessibleRole.ROLE_MATHML_OPERATOR,
  Ci.nsIAccessibleRole.ROLE_MATHML_TEXT,
  Ci.nsIAccessibleRole.ROLE_MATHML_STRING_LITERAL,
  Ci.nsIAccessibleRole.ROLE_MATHML_GLYPH,
  Ci.nsIAccessibleRole.ROLE_MENUITEM,
  Ci.nsIAccessibleRole.ROLE_OPTION,
  Ci.nsIAccessibleRole.ROLE_OUTLINEITEM,
  Ci.nsIAccessibleRole.ROLE_PAGETAB,
  Ci.nsIAccessibleRole.ROLE_PARENT_MENUITEM,
  Ci.nsIAccessibleRole.ROLE_PUSHBUTTON,
  Ci.nsIAccessibleRole.ROLE_RADIOBUTTON,
  Ci.nsIAccessibleRole.ROLE_RADIO_MENU_ITEM,
  Ci.nsIAccessibleRole.ROLE_RICH_OPTION,
  Ci.nsIAccessibleRole.ROLE_ROW,
  Ci.nsIAccessibleRole.ROLE_ROWHEADER,
  Ci.nsIAccessibleRole.ROLE_SUMMARY,
  Ci.nsIAccessibleRole.ROLE_SWITCH,
  Ci.nsIAccessibleRole.ROLE_TERM,
  Ci.nsIAccessibleRole.ROLE_TOGGLE_BUTTON,
  Ci.nsIAccessibleRole.ROLE_TOOLTIP,
]);

const IS_OSX = Services.appinfo.OS === "Darwin";

const {
  SCORES: { BEST_PRACTICES, FAIL, WARNING },
} = accessibility;

/**
 * Helper function that determines if nsIAccessible object is in stale state. When an
 * object is stale it means its subtree is not up to date.
 *
 * @param  {nsIAccessible}  accessible
 *         object to be tested.
 * @return {Boolean}
 *         True if accessible object is stale, false otherwise.
 */

function isStale(accessible) {
  const extraState = {};
  accessible.getState({}, extraState);
  // extraState.value is a bitmask. We are applying bitwise AND to mask out
  // irrelevant states.
  return !!(extraState.value & Ci.nsIAccessibleStates.EXT_STATE_STALE);
}

/**
 * Get accessibility audit starting with the passed accessible object as a root.
 *
 * @param {Object} acc
 *        AccessibileActor to be used as the root for the audit.
 * @param {Object} options
 *        Options for running audit, may include:
 *        - types: Array of audit types to be performed during audit.
 * @param {Map} report
 *        An accumulator map to be used to store audit information.
 * @param {Object} progress
 *        An audit project object that is used to track the progress of the
 *        audit and send progress "audit-event" events to the client.
 */

function getAudit(acc, options, report, progress) {
  if (acc.isDefunct) {
    return;
  }

  // Audit returns a promise, save the actual value in the report.
  report.set(
    acc,
    acc.audit(options).then(result => {
      report.set(acc, result);
      progress.increment();
    })
  );

  for (const child of acc.children()) {
    getAudit(child, options, report, progress);
  }
}

/**
 * A helper class that is used to track audit progress and send progress events
 * to the client.
 */

class AuditProgress {
  constructor(walker) {
    this.completed = 0;
    this.percentage = 0;
    this.walker = walker;
  }

  setTotal(size) {
    this.size = size;
  }

  notify() {
    this.walker.emit("audit-event", {
      type: "progress",
      progress: {
        total: this.size,
        percentage: this.percentage,
        completed: this.completed,
      },
    });
  }

  increment() {
    this.completed++;
    const { completed, size } = this;
    if (!size) {
      return;
    }

    const percentage = Math.round((completed / size) * 100);
    if (percentage > this.percentage) {
      this.percentage = percentage;
      this.notify();
    }
  }

  destroy() {
    this.walker = null;
  }
}

/**
 * The AccessibleWalkerActor stores a cache of AccessibleActors that represent
 * accessible objects in a given document.
 *
 * It is also responsible for implicitely initializing and shutting down
 * accessibility engine by storing a reference to the XPCOM accessibility
 * service.
 */

class AccessibleWalkerActor extends Actor {
  constructor(conn, targetActor) {
    super(conn, accessibleWalkerSpec);
    this.targetActor = targetActor;
    this.refMap = new Map();
    this._loadedSheets = new WeakMap();
    this.setA11yServiceGetter();
    this.onPick = this.onPick.bind(this);
    this.onHovered = this.onHovered.bind(this);
    this._preventContentEvent = this._preventContentEvent.bind(this);
    this.onKey = this.onKey.bind(this);
    this.onFocusIn = this.onFocusIn.bind(this);
    this.onFocusOut = this.onFocusOut.bind(this);
    this.onHighlighterEvent = this.onHighlighterEvent.bind(this);
  }

  get highlighter() {
    if (!this._highlighter) {
      this._highlighter = new CustomHighlighterActor(
        this,
        lazy.TYPES.ACCESSIBLE
      );

      this.manage(this._highlighter);
      this._highlighter.on("highlighter-event"this.onHighlighterEvent);
    }

    return this._highlighter;
  }

  get tabbingOrderHighlighter() {
    if (!this._tabbingOrderHighlighter) {
      this._tabbingOrderHighlighter = new CustomHighlighterActor(
        this,
        lazy.TYPES.TABBING_ORDER
      );

      this.manage(this._tabbingOrderHighlighter);
    }

    return this._tabbingOrderHighlighter;
  }

  setA11yServiceGetter() {
    DevToolsUtils.defineLazyGetter(this"a11yService", () => {
      Services.obs.addObserver(this"accessible-event");
      return Cc["@mozilla.org/accessibilityService;1"].getService(
        Ci.nsIAccessibilityService
      );
    });
  }

  get rootWin() {
    return this.targetActor && this.targetActor.window;
  }

  get rootDoc() {
    return this.targetActor && this.targetActor.window.document;
  }

  get isXUL() {
    return isXUL(this.rootWin);
  }

  get colorMatrix() {
    if (!this.targetActor.docShell) {
      return null;
    }

    const colorMatrix = this.targetActor.docShell.getColorMatrix();
    if (
      colorMatrix.length === 0 ||
      colorMatrix === COLOR_TRANSFORMATION_MATRICES.NONE
    ) {
      return null;
    }

    return colorMatrix;
  }

  reset() {
    try {
      Services.obs.removeObserver(this"accessible-event");
    } catch (e) {
      // Accessible event observer might not have been initialized if a11y
      // service was never used.
    }

    this.cancelPick();

    // Clean up accessible actors cache.
    this.clearRefs();

    this._childrenPromise = null;
    delete this.a11yService;
    this.setA11yServiceGetter();
  }

  /**
   * Remove existing cache (of accessible actors) from tree.
   */

  clearRefs() {
    for (const actor of this.refMap.values()) {
      actor.destroy();
    }
  }

  destroy() {
    super.destroy();

    this.reset();

    if (this._highlighter) {
      this._highlighter.off("highlighter-event"this.onHighlighterEvent);
      this._highlighter = null;
    }

    if (this._tabbingOrderHighlighter) {
      this._tabbingOrderHighlighter = null;
    }

    this.targetActor = null;
    this.refMap = null;
  }

  getRef(rawAccessible) {
    return this.refMap.get(rawAccessible);
  }

  addRef(rawAccessible) {
    let actor = this.refMap.get(rawAccessible);
    if (actor) {
      return actor;
    }

    actor = new AccessibleActor(this, rawAccessible);
    // Add the accessible actor as a child of this accessible walker actor,
    // assigning it an actorID.
    this.manage(actor);
    this.refMap.set(rawAccessible, actor);

    return actor;
  }

  /**
   * Clean up accessible actors cache for a given accessible's subtree.
   *
   * @param  {null|nsIAccessible} rawAccessible
   */

  purgeSubtree(rawAccessible) {
    if (!rawAccessible) {
      return;
    }

    try {
      for (
        let child = rawAccessible.firstChild;
        child;
        child = child.nextSibling
      ) {
        this.purgeSubtree(child);
      }
    } catch (e) {
      // rawAccessible or its descendants are defunct.
    }

    const actor = this.getRef(rawAccessible);
    if (actor) {
      actor.destroy();
    }
  }

  unmanage(actor) {
    if (actor instanceof AccessibleActor) {
      this.refMap.delete(actor.rawAccessible);
    }
    Actor.prototype.unmanage.call(this, actor);
  }

  /**
   * A helper method. Accessibility walker is assumed to have only 1 child which
   * is the top level document.
   */

  async children() {
    if (this._childrenPromise) {
      return this._childrenPromise;
    }

    this._childrenPromise = Promise.all([this.getDocument()]);
    const children = await this._childrenPromise;
    this._childrenPromise = null;
    return children;
  }

  /**
   * A promise for a root document accessible actor that only resolves when its
   * corresponding document accessible object is fully loaded.
   *
   * @return {Promise}
   */

  getDocument() {
    if (!this.rootDoc || !this.rootDoc.documentElement) {
      return this.once("document-ready").then(docAcc => this.addRef(docAcc));
    }

    if (this.isXUL) {
      const doc = this.addRef(this.getRawAccessibleFor(this.rootDoc));
      return Promise.resolve(doc);
    }

    const doc = this.getRawAccessibleFor(this.rootDoc);

    // For non-visible same-process iframes we don't get a document and
    // won't get a "document-ready" event.
    if (!doc && !this.rootWin.windowGlobalChild.isProcessRoot) {
      // We can ignore such document as there won't be anything to audit in them.
      return null;
    }

    if (!doc || isStale(doc)) {
      return this.once("document-ready").then(docAcc => this.addRef(docAcc));
    }

    return Promise.resolve(this.addRef(doc));
  }

  /**
   * Get an accessible actor for a domnode actor.
   * @param  {Object} domNode
   *         domnode actor for which accessible actor is being created.
   * @return {Promse}
   *         A promise that resolves when accessible actor is created for a
   *         domnode actor.
   */

  getAccessibleFor(domNode) {
    // We need to make sure that the document is loaded processed by a11y first.
    return this.getDocument().then(() => {
      const rawAccessible = this.getRawAccessibleFor(domNode.rawNode);
      // Not all DOM nodes have corresponding accessible objects. It's usually
      // the case where there is no semantics or relevance to the accessibility
      // client.
      if (!rawAccessible) {
        return null;
      }

      return this.addRef(rawAccessible);
    });
  }

  /**
   * Get a raw accessible object for a raw node.
   * @param  {DOMNode} rawNode
   *         Raw node for which accessible object is being retrieved.
   * @return {nsIAccessible}
   *         Accessible object for a given DOMNode.
   */

  getRawAccessibleFor(rawNode) {
    // Accessible can only be retrieved iff accessibility service is enabled.
    if (!Services.appinfo.accessibilityEnabled) {
      return null;
    }

    return this.a11yService.getAccessibleFor(rawNode);
  }

  async getAncestry(accessible) {
    if (!accessible || accessible.indexInParent === -1) {
      return [];
    }
    const doc = await this.getDocument();
    if (!doc) {
      return [];
    }

    const ancestry = [];
    if (accessible === doc) {
      return ancestry;
    }

    try {
      let parent = accessible;
      while (parent && (parent = parent.parentAcc) && parent != doc) {
        ancestry.push(parent);
      }
      ancestry.push(doc);
    } catch (error) {
      throw new Error(`Failed to get ancestor for ${accessible}: ${error}`);
    }

    return ancestry.map(parent => ({
      accessible: parent,
      children: parent.children(),
    }));
  }

  /**
   * Run accessibility audit and return relevant ancestries for AccessibleActors
   * that have non-empty audit checks.
   *
   * @param  {Object} options
   *         Options for running audit, may include:
   *         - types: Array of audit types to be performed during audit.
   *
   * @return {Promise}
   *         A promise that resolves when the audit is complete and all relevant
   *         ancestries are calculated.
   */

  async audit(options) {
    const doc = await this.getDocument();
    if (!doc) {
      return [];
    }

    const report = new Map();
    this._auditProgress = new AuditProgress(this);
    getAudit(doc, options, report, this._auditProgress);
    this._auditProgress.setTotal(report.size);
    await Promise.all(report.values());

    const ancestries = [];
    for (const [acc, audit] of report.entries()) {
      // Filter out audits that have no failing checks.
      if (
        audit &&
        Object.values(audit).some(
          check =>
            check != null &&
            !check.error &&
            [BEST_PRACTICES, FAIL, WARNING].includes(check.score)
        )
      ) {
        ancestries.push(this.getAncestry(acc));
      }
    }

    return Promise.all(ancestries);
  }

  /**
   * Start accessibility audit. The result of this function will not be an audit
   * report. Instead, an "audit-event" event will be fired when the audit is
   * completed or fails.
   *
   * @param {Object} options
   *        Options for running audit, may include:
   *        - types: Array of audit types to be performed during audit.
   */

  startAudit(options) {
    // Audit is already running, wait for the "audit-event" event.
    if (this._auditing) {
      return;
    }

    this._auditing = this.audit(options)
      // We do not want to block on audit request, instead fire "audit-event"
      // event when internal audit is finished or failed.
      .then(ancestries =>
        this.emit("audit-event", {
          type: "completed",
          ancestries,
        })
      )
      .catch(() => this.emit("audit-event", { type: "error" }))
      .finally(() => {
        this._auditing = null;
        if (this._auditProgress) {
          this._auditProgress.destroy();
          this._auditProgress = null;
        }
      });
  }

  onHighlighterEvent(data) {
    this.emit("highlighter-event", data);
  }

  /**
   * Accessible event observer function.
   *
   * @param {Ci.nsIAccessibleEvent} subject
   *                                      accessible event object.
   */

  // eslint-disable-next-line complexity
  observe(subject) {
    const event = subject.QueryInterface(Ci.nsIAccessibleEvent);
    const rawAccessible = event.accessible;
    const accessible = this.getRef(rawAccessible);

    if (rawAccessible instanceof Ci.nsIAccessibleDocument && !accessible) {
      const rootDocAcc = this.getRawAccessibleFor(this.rootDoc);
      if (rawAccessible === rootDocAcc && !isStale(rawAccessible)) {
        this.clearRefs();
        // If it's a top level document notify listeners about the document
        // being ready.
        events.emit(this"document-ready", rawAccessible);
      }
    }

    switch (event.eventType) {
      case EVENT_STATE_CHANGE:
        const { state, isEnabled } = event.QueryInterface(
          Ci.nsIAccessibleStateChangeEvent
        );
        const isBusy = state & Ci.nsIAccessibleStates.STATE_BUSY;
        if (accessible) {
          // Only propagate state change events for active accessibles.
          if (isBusy && isEnabled) {
            if (rawAccessible instanceof Ci.nsIAccessibleDocument) {
              // Remove existing cache from tree.
              this.clearRefs();
            }
            return;
          }
          events.emit(accessible, "states-change", accessible.states);
        }

        break;
      case EVENT_NAME_CHANGE:
        if (accessible) {
          events.emit(
            accessible,
            "name-change",
            rawAccessible.name,
            event.DOMNode == this.rootDoc
              ? undefined
              : this.getRef(rawAccessible.parent)
          );
        }
        break;
      case EVENT_VALUE_CHANGE:
        if (accessible) {
          events.emit(accessible, "value-change", rawAccessible.value);
        }
        break;
      case EVENT_DESCRIPTION_CHANGE:
        if (accessible) {
          events.emit(
            accessible,
            "description-change",
            rawAccessible.description
          );
        }
        break;
      case EVENT_REORDER:
        if (accessible) {
          accessible
            .children()
            .forEach(child =>
              events.emit(child, "index-in-parent-change", child.indexInParent)
            );
          events.emit(accessible, "reorder", rawAccessible.childCount);
        }
        break;
      case EVENT_HIDE:
        if (event.DOMNode == this.rootDoc) {
          this.clearRefs();
        } else {
          this.purgeSubtree(rawAccessible);
        }
        break;
      case EVENT_DEFACTION_CHANGE:
      case EVENT_ACTION_CHANGE:
        if (accessible) {
          events.emit(accessible, "actions-change", accessible.actions);
        }
        break;
      case EVENT_TEXT_CHANGED:
      case EVENT_TEXT_INSERTED:
      case EVENT_TEXT_REMOVED:
        if (accessible) {
          events.emit(accessible, "text-change");
          if (NAME_FROM_SUBTREE_RULE_ROLES.has(rawAccessible.role)) {
            events.emit(
              accessible,
              "name-change",
              rawAccessible.name,
              event.DOMNode == this.rootDoc
                ? undefined
                : this.getRef(rawAccessible.parent)
            );
          }
        }
        break;
      case EVENT_DOCUMENT_ATTRIBUTES_CHANGED:
      case EVENT_OBJECT_ATTRIBUTE_CHANGED:
      case EVENT_TEXT_ATTRIBUTE_CHANGED:
        if (accessible) {
          events.emit(accessible, "attributes-change", accessible.attributes);
        }
        break;
      // EVENT_ACCELERATOR_CHANGE is currently not fired by gecko accessibility.
      case EVENT_ACCELERATOR_CHANGE:
        if (accessible) {
          events.emit(
            accessible,
            "shortcut-change",
            accessible.keyboardShortcut
          );
        }
        break;
      default:
        break;
    }
  }

  /**
   * Ensure that nothing interferes with the audit for an accessible object
   * (CSS, overlays) by load accessibility highlighter style sheet used for
   * preventing transitions and applying transparency when calculating colour
   * contrast as well as temporarily hiding accessible highlighter overlay.
   * @param  {Object} win
   *         Window where highlighting happens.
   */

  async clearStyles(win) {
    const requests = this._loadedSheets.get(win);
    if (requests != null) {
      this._loadedSheets.set(win, requests + 1);
      return;
    }

    // Disable potential mouse driven transitions (This is important because accessibility
    // highlighter temporarily modifies text color related CSS properties. In case where
    // there are transitions that affect them, there might be unexpected side effects when
    // taking a snapshot for contrast measurement).
    loadSheetForBackgroundCalculation(win);
    this._loadedSheets.set(win, 1);
    await this.hideHighlighter();
  }

  /**
   * Restore CSS and overlays that could've interfered with the audit for an
   * accessible object by unloading accessibility highlighter style sheet used
   * for preventing transitions and applying transparency when calculating
   * colour contrast and potentially restoring accessible highlighter overlay.
   * @param  {Object} win
   *         Window where highlighting was happenning.
   */

  async restoreStyles(win) {
    const requests = this._loadedSheets.get(win);
    if (!requests) {
      return;
    }

    if (requests > 1) {
      this._loadedSheets.set(win, requests - 1);
      return;
    }

    await this.showHighlighter();
    removeSheetForBackgroundCalculation(win);
    this._loadedSheets.delete(win);
  }

  async hideHighlighter() {
    // TODO: Fix this workaround that temporarily removes higlighter bounds
    // overlay that can interfere with the contrast ratio calculation.
    if (this._highlighter) {
      const highlighter = this._highlighter.instance;
      await highlighter.isReady;
      highlighter.hideAccessibleBounds();
    }
  }

  async showHighlighter() {
    // TODO: Fix this workaround that temporarily removes higlighter bounds
    // overlay that can interfere with the contrast ratio calculation.
    if (this._highlighter) {
      const highlighter = this._highlighter.instance;
      await highlighter.isReady;
      highlighter.showAccessibleBounds();
    }
  }

  /**
   * Public method used to show an accessible object highlighter on the client
   * side.
   *
   * @param  {Object} accessible
   *         AccessibleActor to be highlighted.
   * @param  {Object} options
   *         Object used for passing options. Available options:
   *         - duration {Number}
   *                    Duration of time that the highlighter should be shown.
   * @return {Boolean}
   *         True if highlighter shows the accessible object.
   */

  async highlightAccessible(accessible, options = {}) {
    this.unhighlight();
    // Do not highlight if accessible is dead.
    if (!accessible || accessible.isDefunct || accessible.indexInParent < 0) {
      return false;
    }

    this._highlightingAccessible = accessible;
    const { bounds } = accessible;
    if (!bounds) {
      return false;
    }

    const { DOMNode: rawNode } = accessible.rawAccessible;
    const audit = await accessible.audit();
    if (this._highlightingAccessible !== accessible) {
      return false;
    }

    const { name, role } = accessible;
    const { highlighter } = this;
    await highlighter.instance.isReady;
    if (this._highlightingAccessible !== accessible) {
      return false;
    }

    const shown = highlighter.show(
      { rawNode },
      { ...options, ...bounds, name, role, audit, isXUL: this.isXUL }
    );
    this._highlightingAccessible = null;

    return shown;
  }

  /**
   * Public method used to hide an accessible object highlighter on the client
   * side.
   */

  unhighlight() {
    if (!this._highlighter) {
      return;
    }

    this.highlighter.hide();
    this._highlightingAccessible = null;
  }

  /**
   * Picking state that indicates if picking is currently enabled and, if so,
   * what the current and hovered accessible objects are.
   */

  _isPicking = false;
  _currentAccessible = null;

  /**
   * Check is event handling is allowed.
   */

  _isEventAllowed({ view }) {
    return this.rootWin.isChromeWindow || isWindowIncluded(this.rootWin, view);
  }

  /**
   * Check if the DOM event received when picking shold be ignored.
   * @param {Event} event
   */

  _ignoreEventWhenPicking(event) {
    return (
      !this._isPicking ||
      // If the DOM event is about a remote frame, only the WalkerActor for that
      // remote frame target should emit RDP events (hovered/picked/...). And
      // all other WalkerActor for intermediate iframe and top level document
      // targets should stay silent.
      isFrameWithChildTarget(
        this.targetActor,
        event.originalTarget || event.target
      )
    );
  }

  _preventContentEvent(event) {
    if (this._ignoreEventWhenPicking(event)) {
      return;
    }

    event.stopPropagation();
    event.preventDefault();

    const target = event.originalTarget || event.target;
    if (target !== this._currentTarget) {
      this._resetStateAndReleaseTarget();
      this._currentTarget = target;
      // We use InspectorUtils to save the original hover content state of the target
      // element (that includes its hover state). In order to not trigger any visual
      // changes to the element that depend on its hover state we remove the state while
      // the element is the most current target of the highlighter.
      //
      // TODO: This logic can be removed if/when we can use elementsAtPoint API for
      // determining topmost DOMNode that corresponds to specific coordinates. We would
      // then be able to use a highlighter overlay that would prevent all pointer events
      // to content but still render highlighter for the node/element correctly.
      this._currentTargetHoverState =
        InspectorUtils.getContentState(target) & kStateHover;
      InspectorUtils.removeContentState(target, kStateHover);
    }
  }

  /**
   * Click event handler for when picking is enabled.
   *
   * @param  {Object} event
   *         Current click event.
   */

  onPick(event) {
    if (this._ignoreEventWhenPicking(event)) {
      return;
    }

    this._preventContentEvent(event);
    if (!this._isEventAllowed(event)) {
      return;
    }

    // If shift is pressed, this is only a preview click, send the event to
    // the client, but don't stop picking.
    if (event.shiftKey) {
      if (!this._currentAccessible) {
        this._currentAccessible = this._findAndAttachAccessible(event);
      }
      events.emit(this"picker-accessible-previewed"this._currentAccessible);
      return;
    }

    this._unsetPickerEnvironment();
    this._isPicking = false;
    if (!this._currentAccessible) {
      this._currentAccessible = this._findAndAttachAccessible(event);
    }
    events.emit(this"picker-accessible-picked"this._currentAccessible);
  }

  /**
   * Hover event handler for when picking is enabled.
   *
   * @param  {Object} event
   *         Current hover event.
   */

  async onHovered(event) {
    if (this._ignoreEventWhenPicking(event)) {
      return;
    }

    this._preventContentEvent(event);
    if (!this._isEventAllowed(event)) {
      return;
    }

    const accessible = this._findAndAttachAccessible(event);
    if (!accessible || this._currentAccessible === accessible) {
      return;
    }

    this._currentAccessible = accessible;
    // Highlight current accessible and by the time we are done, if accessible that was
    // highlighted is not current any more (user moved the mouse to a new node) highlight
    // the most current accessible again.
    const shown = await this.highlightAccessible(accessible);
    if (this._isPicking && shown && accessible === this._currentAccessible) {
      events.emit(this"picker-accessible-hovered", accessible);
    }
  }

  /**
   * Keyboard event handler for when picking is enabled.
   *
   * @param  {Object} event
   *         Current keyboard event.
   */

  onKey(event) {
    if (!this._currentAccessible || this._ignoreEventWhenPicking(event)) {
      return;
    }

    this._preventContentEvent(event);
    if (!this._isEventAllowed(event)) {
      return;
    }

    /**
     * KEY: Action/scope
     * ENTER/CARRIAGE_RETURN: Picks current accessible
     * ESC/CTRL+SHIFT+C: Cancels picker
     */

    switch (event.keyCode) {
      // Select the element.
      case event.DOM_VK_RETURN:
        this.onPick(event);
        break;
      // Cancel pick mode.
      case event.DOM_VK_ESCAPE:
        this.cancelPick();
        events.emit(this"picker-accessible-canceled");
        break;
      case event.DOM_VK_C:
        if (
          (IS_OSX && event.metaKey && event.altKey) ||
          (!IS_OSX && event.ctrlKey && event.shiftKey)
        ) {
          this.cancelPick();
          events.emit(this"picker-accessible-canceled");
        }
        break;
      default:
        break;
    }
  }

  /**
   * Picker method that starts picker content listeners.
   */

  pick() {
    if (!this._isPicking) {
      this._isPicking = true;
      this._setPickerEnvironment();
    }
  }

  /**
   * This pick method also focuses the highlighter's target window.
   */

  pickAndFocus() {
    this.pick();
    this.rootWin.focus();
  }

  attachAccessible(rawAccessible, accessibleDocument) {
    // If raw accessible object is defunct or detached, no need to cache it and
    // its ancestry.
    if (
      !rawAccessible ||
      isDefunct(rawAccessible) ||
      rawAccessible.indexInParent < 0
    ) {
      return null;
    }

    const accessible = this.addRef(rawAccessible);
    // There is a chance that ancestry lookup can fail if the accessible is in
    // the detached subtree. At that point the root accessible object would be
    // defunct and accessing it via parent property will throw.
    try {
      let parent = accessible;
      while (parent && parent.rawAccessible != accessibleDocument) {
        parent = parent.parentAcc;
      }
    } catch (error) {
      throw new Error(`Failed to get ancestor for ${accessible}: ${error}`);
    }

    return accessible;
  }

  /**
   * Find deepest accessible object that corresponds to the screen coordinates of the
   * mouse pointer and attach it to the AccessibilityWalker tree.
   *
   * @param  {Object} event
   *         Correspoinding content event.
   * @return {null|Object}
   *         Accessible object, if available, that corresponds to a DOM node.
   */

  _findAndAttachAccessible(event) {
    const target = event.originalTarget || event.target;
    const win = target.ownerGlobal;
    // This event might be inside a sub-document, so don't use this.rootDoc.
    const docAcc = this.getRawAccessibleFor(win.document);
    // If the target is inside a pop-up widget, we need to query the pop-up
    // Accessible, not the DocAccessible. The DocAccessible can't hit test
    // inside pop-ups.
    const popup = win.isChromeWindow ? target.closest("panel") : null;
    const containerAcc = popup ? this.getRawAccessibleFor(popup) : docAcc;
    const { devicePixelRatio } = this.rootWin;
    const rawAccessible = containerAcc.getDeepestChildAtPointInProcess(
      event.screenX * devicePixelRatio,
      event.screenY * devicePixelRatio
    );
    return this.attachAccessible(rawAccessible, docAcc);
  }

  /**
   * Start picker content listeners.
   */

  _setPickerEnvironment() {
    const target = this.targetActor.chromeEventHandler;
    target.addEventListener("mousemove"this.onHovered, true);
    target.addEventListener("click"this.onPick, true);
    target.addEventListener("mousedown"this._preventContentEvent, true);
    target.addEventListener("mouseup"this._preventContentEvent, true);
    target.addEventListener("mouseover"this._preventContentEvent, true);
    target.addEventListener("mouseout"this._preventContentEvent, true);
    target.addEventListener("mouseleave"this._preventContentEvent, true);
    target.addEventListener("mouseenter"this._preventContentEvent, true);
    target.addEventListener("dblclick"this._preventContentEvent, true);
    target.addEventListener("keydown"this.onKey, true);
    target.addEventListener("keyup"this._preventContentEvent, true);
  }

  /**
   * If content is still alive, stop picker content listeners, reset the hover state for
   * last target element.
   */

  _unsetPickerEnvironment() {
    const target = this.targetActor.chromeEventHandler;

    if (!target) {
      return;
    }

    target.removeEventListener("mousemove"this.onHovered, true);
    target.removeEventListener("click"this.onPick, true);
    target.removeEventListener("mousedown"this._preventContentEvent, true);
    target.removeEventListener("mouseup"this._preventContentEvent, true);
    target.removeEventListener("mouseover"this._preventContentEvent, true);
    target.removeEventListener("mouseout"this._preventContentEvent, true);
    target.removeEventListener("mouseleave"this._preventContentEvent, true);
    target.removeEventListener("mouseenter"this._preventContentEvent, true);
    target.removeEventListener("dblclick"this._preventContentEvent, true);
    target.removeEventListener("keydown"this.onKey, true);
    target.removeEventListener("keyup"this._preventContentEvent, true);

    this._resetStateAndReleaseTarget();
  }

  /**
   * When using accessibility highlighter, we keep track of the most current event pointer
   * event target. In order to update or release the target, we need to make sure we set
   * the content state (using InspectorUtils) to its original value.
   *
   * TODO: This logic can be removed if/when we can use elementsAtPoint API for
   * determining topmost DOMNode that corresponds to specific coordinates. We would then
   * be able to use a highlighter overlay that would prevent all pointer events to content
   * but still render highlighter for the node/element correctly.
   */

  _resetStateAndReleaseTarget() {
    if (!this._currentTarget) {
      return;
    }

    try {
      if (this._currentTargetHoverState) {
        InspectorUtils.setContentState(this._currentTarget, kStateHover);
      }
    } catch (e) {
      // DOMNode is already dead.
    }

    this._currentTarget = null;
    this._currentTargetState = null;
  }

  /**
   * Cacncel picker pick. Remvoe all content listeners and hide the highlighter.
   */

  cancelPick() {
    this.unhighlight();

    if (this._isPicking) {
      this._unsetPickerEnvironment();
      this._isPicking = false;
      this._currentAccessible = null;
    }
  }

  /**
   * Indicates that the tabbing order current active element (focused) is being
   * tracked.
   */

  _isTrackingTabbingOrderFocus = false;

  /**
   * Current focused element in the tabbing order.
   */

  _currentFocusedTabbingOrder = null;

  /**
   * Focusin event handler for when interacting with tabbing order overlay.
   *
   * @param  {Object} event
   *         Most recent focusin event.
   */

  async onFocusIn(event) {
    if (!this._isTrackingTabbingOrderFocus) {
      return;
    }

    const target = event.originalTarget || event.target;
    if (target === this._currentFocusedTabbingOrder) {
      return;
    }

    this._currentFocusedTabbingOrder = target;
    this.tabbingOrderHighlighter._highlighter.updateFocus({
      node: target,
      focused: true,
    });
  }

  /**
   * Focusout event handler for when interacting with tabbing order overlay.
   *
   * @param  {Object} event
   *         Most recent focusout event.
   */

  async onFocusOut(event) {
    if (
      !this._isTrackingTabbingOrderFocus ||
      !this._currentFocusedTabbingOrder
    ) {
      return;
    }

    const target = event.originalTarget || event.target;
    // Sanity check.
    if (target !== this._currentFocusedTabbingOrder) {
      console.warn(
        `focusout target: ${target} does not match current focused element in tabbing order: ${this._currentFocusedTabbingOrder}`
      );
    }

    this.tabbingOrderHighlighter._highlighter.updateFocus({
      node: this._currentFocusedTabbingOrder,
      focused: false,
    });
    this._currentFocusedTabbingOrder = null;
  }

  /**
   * Show tabbing order overlay for a given target.
   *
   * @param  {Object} elm
   *         domnode actor to be used as the starting point for generating the
   *         tabbing order.
   * @param  {Number} index
   *         Starting index for the tabbing order.
   *
   * @return {JSON}
   *         Tabbing order information for the last element in the tabbing
   *         order. It includes a ContentDOMReference for the node and a tabbing
   *         index. If we are at the end of the tabbing order for the top level
   *         content document, the ContentDOMReference will be null. If focus
   *         manager discovered a remote IFRAME, then the ContentDOMReference
   *         references the IFRAME itself.
   */

  showTabbingOrder(elm, index) {
    // Start track focus related events (only once). `showTabbingOrder` will be
    // called multiple times for a given target if it contains other remote
    // targets.
    if (!this._isTrackingTabbingOrderFocus) {
      this._isTrackingTabbingOrderFocus = true;
      const target = this.targetActor.chromeEventHandler;
      target.addEventListener("focusin"this.onFocusIn, true);
      target.addEventListener("focusout"this.onFocusOut, true);
    }

    return this.tabbingOrderHighlighter.show(elm, { index });
  }

  /**
   * Hide tabbing order overlay for a given target.
   */

  hideTabbingOrder() {
    if (!this._tabbingOrderHighlighter) {
      return;
    }

    this.tabbingOrderHighlighter.hide();
    if (!this._isTrackingTabbingOrderFocus) {
      return;
    }

    this._isTrackingTabbingOrderFocus = false;
    this._currentFocusedTabbingOrder = null;
    const target = this.targetActor.chromeEventHandler;
    if (target) {
      target.removeEventListener("focusin"this.onFocusIn, true);
      target.removeEventListener("focusout"this.onFocusOut, true);
    }
  }
}

exports.AccessibleWalkerActor = AccessibleWalkerActor;

Messung V0.5
C=94 H=98 G=95

¤ Dauer der Verarbeitung: 0.19 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge