tor-browser

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

accessible.js (10304B)


      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  AutoRefreshHighlighter,
      9 } = require("resource://devtools/server/actors/highlighters/auto-refresh.js");
     10 const {
     11  CanvasFrameAnonymousContentHelper,
     12  isNodeValid,
     13 } = require("resource://devtools/server/actors/highlighters/utils/markup.js");
     14 const {
     15  TEXT_NODE,
     16  DOCUMENT_NODE,
     17 } = require("resource://devtools/shared/dom-node-constants.js");
     18 const {
     19  getCurrentZoom,
     20  setIgnoreLayoutChanges,
     21 } = require("resource://devtools/shared/layout/utils.js");
     22 
     23 loader.lazyRequireGetter(
     24  this,
     25  ["getBounds", "getBoundsXUL", "Infobar"],
     26  "resource://devtools/server/actors/highlighters/utils/accessibility.js",
     27  true
     28 );
     29 
     30 /**
     31 * The AccessibleHighlighter draws the bounds of an accessible object.
     32 *
     33 * Usage example:
     34 *
     35 * let h = new AccessibleHighlighter(env);
     36 * h.show(node, { x, y, w, h, [duration] });
     37 * h.hide();
     38 * h.destroy();
     39 *
     40 * @param {number} options.x
     41 *        X coordinate of the top left corner of the accessible object
     42 * @param {number} options.y
     43 *        Y coordinate of the top left corner of the accessible object
     44 * @param {number} options.w
     45 *        Width of the the accessible object
     46 * @param {number} options.h
     47 *        Height of the the accessible object
     48 * @param {number} options.duration
     49 *        Duration of time that the highlighter should be shown.
     50 * @param {string | null} options.name
     51 *        Name of the the accessible object
     52 * @param {string} options.role
     53 *        Role of the the accessible object
     54 *
     55 * Structure:
     56 * <div class="highlighter-container" aria-hidden="true">
     57 *   <div class="accessible-root">
     58 *     <svg class="accessible-elements" hidden="true">
     59 *       <path class="accessible-bounds" points="..." />
     60 *     </svg>
     61 *     <div class="accessible-infobar-container">
     62 *      <div class="accessible-infobar">
     63 *        <div class="accessible-infobar-text">
     64 *          <span class="accessible-infobar-role">Accessible Role</span>
     65 *          <span class="accessible-infobar-name">Accessible Name</span>
     66 *        </div>
     67 *      </div>
     68 *     </div>
     69 *   </div>
     70 * </div>
     71 */
     72 class AccessibleHighlighter extends AutoRefreshHighlighter {
     73  constructor(highlighterEnv) {
     74    super(highlighterEnv);
     75    this.accessibleInfobar = new Infobar(this);
     76 
     77    this.markup = new CanvasFrameAnonymousContentHelper(
     78      this.highlighterEnv,
     79      this._buildMarkup.bind(this),
     80      {
     81        contentRootHostClassName: "devtools-highlighter-accessible",
     82      }
     83    );
     84    this.isReady = this.markup.initialize();
     85 
     86    this.onPageHide = this.onPageHide.bind(this);
     87    this.onWillNavigate = this.onWillNavigate.bind(this);
     88 
     89    this.highlighterEnv.on("will-navigate", this.onWillNavigate);
     90 
     91    this.pageListenerTarget = highlighterEnv.pageListenerTarget;
     92    this.pageListenerTarget.addEventListener("pagehide", this.onPageHide);
     93  }
     94 
     95  /**
     96   * Static getter that indicates that AccessibleHighlighter supports
     97   * highlighting in XUL windows.
     98   */
     99  static get XULSupported() {
    100    return true;
    101  }
    102 
    103  get supportsSimpleHighlighters() {
    104    return true;
    105  }
    106 
    107  /**
    108   * Build highlighter markup.
    109   *
    110   * @return {object} Container element for the highlighter markup.
    111   */
    112  _buildMarkup() {
    113    const container = this.markup.createNode({
    114      attributes: {
    115        class: "highlighter-container",
    116        "aria-hidden": "true",
    117      },
    118    });
    119 
    120    this.rootEl = this.markup.createNode({
    121      parent: container,
    122      attributes: {
    123        id: "accessible-root",
    124        class:
    125          "accessible-root" +
    126          (this.highlighterEnv.useSimpleHighlightersForReducedMotion
    127            ? " use-simple-highlighters"
    128            : ""),
    129      },
    130    });
    131 
    132    // Build the SVG element.
    133    const svg = this.markup.createSVGNode({
    134      nodeType: "svg",
    135      parent: this.rootEl,
    136      attributes: {
    137        id: "accessible-elements",
    138        width: "100%",
    139        height: "100%",
    140        hidden: "true",
    141      },
    142    });
    143 
    144    this.markup.createSVGNode({
    145      nodeType: "path",
    146      parent: svg,
    147      attributes: {
    148        class: "accessible-bounds",
    149        id: "accessible-bounds",
    150      },
    151    });
    152 
    153    // Build the accessible's infobar markup.
    154    this.accessibleInfobar.buildMarkup(this.rootEl);
    155 
    156    return container;
    157  }
    158 
    159  /**
    160   * Destroy the nodes. Remove listeners.
    161   */
    162  destroy() {
    163    if (this._highlightTimer) {
    164      clearTimeout(this._highlightTimer);
    165      this._highlightTimer = null;
    166    }
    167 
    168    this.highlighterEnv.off("will-navigate", this.onWillNavigate);
    169    this.pageListenerTarget.removeEventListener("pagehide", this.onPageHide);
    170    this.pageListenerTarget = null;
    171 
    172    AutoRefreshHighlighter.prototype.destroy.call(this);
    173 
    174    this.accessibleInfobar.destroy();
    175    this.accessibleInfobar = null;
    176    this.markup.destroy();
    177    this.rootEl = null;
    178  }
    179 
    180  /**
    181   * Find an element in highlighter markup.
    182   *
    183   * @param  {string} id
    184   *         Highlighter markup elemet id attribute.
    185   * @return {DOMNode} Element in the highlighter markup.
    186   */
    187  getElement(id) {
    188    return this.markup.getElement(id);
    189  }
    190 
    191  /**
    192   * Check if node is a valid element, document or text node.
    193   *
    194   * @override
    195   * @param  {DOMNode} node
    196   *         The node to highlight.
    197   * @return {boolean} whether or not node is valid.
    198   */
    199  _isNodeValid(node) {
    200    return (
    201      super._isNodeValid(node) ||
    202      isNodeValid(node, TEXT_NODE) ||
    203      isNodeValid(node, DOCUMENT_NODE)
    204    );
    205  }
    206 
    207  /**
    208   * Show the highlighter on a given accessible.
    209   *
    210   * @return {boolean} True if accessible is highlighted, false otherwise.
    211   */
    212  _show() {
    213    if (this._highlightTimer) {
    214      clearTimeout(this._highlightTimer);
    215      this._highlightTimer = null;
    216    }
    217 
    218    const { duration } = this.options;
    219    const shown = this._update();
    220    if (shown) {
    221      this.emit("highlighter-event", { options: this.options, type: "shown" });
    222      if (duration) {
    223        this._highlightTimer = setTimeout(() => {
    224          this.hide();
    225        }, duration);
    226      }
    227    }
    228 
    229    return shown;
    230  }
    231 
    232  /**
    233   * Update and show accessible bounds for a current accessible.
    234   *
    235   * @return {boolean} True if accessible is highlighted, false otherwise.
    236   */
    237  _update() {
    238    let shown = false;
    239    setIgnoreLayoutChanges(true);
    240 
    241    if (this._updateAccessibleBounds()) {
    242      this._showAccessibleBounds();
    243 
    244      this.accessibleInfobar.show();
    245 
    246      shown = true;
    247    } else {
    248      // Nothing to highlight (0px rectangle like a <script> tag for instance)
    249      this.hide();
    250    }
    251 
    252    setIgnoreLayoutChanges(
    253      false,
    254      this.highlighterEnv.window.document.documentElement
    255    );
    256 
    257    return shown;
    258  }
    259 
    260  /**
    261   * Hide the highlighter.
    262   */
    263  _hide() {
    264    setIgnoreLayoutChanges(true);
    265    this._hideAccessibleBounds();
    266    this.accessibleInfobar.hide();
    267    setIgnoreLayoutChanges(
    268      false,
    269      this.highlighterEnv.window.document.documentElement
    270    );
    271  }
    272 
    273  /**
    274   * Public API method to temporarily hide accessible bounds for things like
    275   * color contrast calculation.
    276   */
    277  hideAccessibleBounds() {
    278    if (this.getElement("accessible-elements").hasAttribute("hidden")) {
    279      return;
    280    }
    281 
    282    this._hideAccessibleBounds();
    283    this._shouldRestoreBoundsVisibility = true;
    284  }
    285 
    286  /**
    287   * Public API method to show accessible bounds in case they were temporarily
    288   * hidden.
    289   */
    290  showAccessibleBounds() {
    291    if (this._shouldRestoreBoundsVisibility) {
    292      this._showAccessibleBounds();
    293    }
    294  }
    295 
    296  /**
    297   * Hide the accessible bounds container.
    298   */
    299  _hideAccessibleBounds() {
    300    this._shouldRestoreBoundsVisibility = null;
    301    setIgnoreLayoutChanges(true);
    302    this.getElement("accessible-elements").setAttribute("hidden", "true");
    303    setIgnoreLayoutChanges(
    304      false,
    305      this.highlighterEnv.window.document.documentElement
    306    );
    307  }
    308 
    309  /**
    310   * Show the accessible bounds container.
    311   */
    312  _showAccessibleBounds() {
    313    this._shouldRestoreBoundsVisibility = null;
    314    if (!this.currentNode || !this.highlighterEnv.window) {
    315      return;
    316    }
    317 
    318    setIgnoreLayoutChanges(true);
    319    this.getElement("accessible-elements").removeAttribute("hidden");
    320    setIgnoreLayoutChanges(
    321      false,
    322      this.highlighterEnv.window.document.documentElement
    323    );
    324  }
    325 
    326  /**
    327   * Get current accessible bounds.
    328   *
    329   * @return {object | null} Returns, if available, positioning and bounds
    330   *                       information for the accessible object.
    331   */
    332  get _bounds() {
    333    let { win, options } = this;
    334    let getBoundsFn = getBounds;
    335    if (this.options.isXUL) {
    336      // Zoom level for the top level browser window does not change and only
    337      // inner frames do. So we need to get the zoom level of the current node's
    338      // parent window.
    339      let zoom = getCurrentZoom(this.currentNode);
    340      zoom *= zoom;
    341      options = { ...options, zoom };
    342      getBoundsFn = getBoundsXUL;
    343      win = this.win.parent.ownerGlobal;
    344    }
    345 
    346    return getBoundsFn(win, options);
    347  }
    348 
    349  /**
    350   * Update accessible bounds for a current accessible. Re-draw highlighter
    351   * markup.
    352   *
    353   * @return {boolean} True if accessible is highlighted, false otherwise.
    354   */
    355  _updateAccessibleBounds() {
    356    const bounds = this._bounds;
    357    if (!bounds) {
    358      this._hide();
    359      return false;
    360    }
    361 
    362    const boundsEl = this.getElement("accessible-bounds");
    363    const { left, right, top, bottom } = bounds;
    364    const path = `M${left},${top} L${right},${top} L${right},${bottom} L${left},${bottom} L${left},${top}`;
    365    boundsEl.setAttribute("d", path);
    366 
    367    // Un-zoom the root wrapper if the page was zoomed.
    368    this.markup.scaleRootElement(this.currentNode, "accessible-elements");
    369 
    370    return true;
    371  }
    372 
    373  /**
    374   * Hide highlighter on page hide.
    375   */
    376  onPageHide({ target }) {
    377    // If a pagehide event is triggered for current window's highlighter, hide
    378    // the highlighter.
    379    if (target.defaultView === this.win) {
    380      this.hide();
    381    }
    382  }
    383 
    384  /**
    385   * Hide highlighter on navigation.
    386   */
    387  onWillNavigate({ isTopLevel }) {
    388    if (isTopLevel) {
    389      this.hide();
    390    }
    391  }
    392 }
    393 
    394 exports.AccessibleHighlighter = AccessibleHighlighter;