tor-browser

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

AccessibilityRow.js (8504B)


      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 "use strict";
      5 
      6 /* global EVENTS */
      7 
      8 // React & Redux
      9 const {
     10  Component,
     11  createFactory,
     12 } = require("resource://devtools/client/shared/vendor/react.mjs");
     13 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     14 const {
     15  findDOMNode,
     16 } = require("resource://devtools/client/shared/vendor/react-dom.mjs");
     17 const {
     18  connect,
     19 } = require("resource://devtools/client/shared/vendor/react-redux.js");
     20 
     21 const TreeRow = ChromeUtils.importESModule(
     22  "resource://devtools/client/shared/components/tree/TreeRow.mjs",
     23  { global: "current" }
     24 ).default;
     25 const AuditFilter = createFactory(
     26  require("resource://devtools/client/accessibility/components/AuditFilter.js")
     27 );
     28 const AuditController = createFactory(
     29  require("resource://devtools/client/accessibility/components/AuditController.js")
     30 );
     31 
     32 // Utils
     33 const {
     34  flashElementOn,
     35  flashElementOff,
     36 } = require("resource://devtools/client/inspector/markup/utils.js");
     37 const { openDocLink } = require("resource://devtools/client/shared/link.js");
     38 const {
     39  PREFS,
     40  VALUE_FLASHING_DURATION,
     41  VALUE_HIGHLIGHT_DURATION,
     42 } = require("resource://devtools/client/accessibility/constants.js");
     43 
     44 const nodeConstants = require("resource://devtools/shared/dom-node-constants.js");
     45 
     46 // Actions
     47 const {
     48  updateDetails,
     49 } = require("resource://devtools/client/accessibility/actions/details.js");
     50 const {
     51  unhighlight,
     52 } = require("resource://devtools/client/accessibility/actions/accessibles.js");
     53 
     54 const {
     55  L10N,
     56 } = require("resource://devtools/client/accessibility/utils/l10n.js");
     57 
     58 loader.lazyRequireGetter(
     59  this,
     60  "Menu",
     61  "resource://devtools/client/framework/menu.js"
     62 );
     63 loader.lazyRequireGetter(
     64  this,
     65  "MenuItem",
     66  "resource://devtools/client/framework/menu-item.js"
     67 );
     68 
     69 const { scrollIntoView } = ChromeUtils.importESModule(
     70  "resource://devtools/client/shared/scroll.mjs"
     71 );
     72 
     73 const JSON_URL_PREFIX = "data:application/json;charset=UTF-8,";
     74 
     75 class HighlightableTreeRowClass extends TreeRow {
     76  shouldComponentUpdate(nextProps) {
     77    const shouldTreeRowUpdate = super.shouldComponentUpdate(nextProps);
     78    if (shouldTreeRowUpdate) {
     79      return shouldTreeRowUpdate;
     80    }
     81 
     82    if (
     83      nextProps.highlighted !== this.props.highlighted ||
     84      nextProps.filtered !== this.props.filtered
     85    ) {
     86      return true;
     87    }
     88 
     89    return false;
     90  }
     91 }
     92 
     93 const HighlightableTreeRow = createFactory(HighlightableTreeRowClass);
     94 
     95 // Component that expands TreeView's own TreeRow and is responsible for
     96 // rendering an accessible object.
     97 class AccessibilityRow extends Component {
     98  static get propTypes() {
     99    return {
    100      ...TreeRow.propTypes,
    101      dispatch: PropTypes.func.isRequired,
    102      toolboxDoc: PropTypes.object.isRequired,
    103      scrollContentNodeIntoView: PropTypes.bool.isRequired,
    104      highlightAccessible: PropTypes.func.isRequired,
    105      unhighlightAccessible: PropTypes.func.isRequired,
    106    };
    107  }
    108 
    109  componentDidMount() {
    110    const {
    111      member: { selected, object },
    112      scrollContentNodeIntoView,
    113    } = this.props;
    114    if (selected) {
    115      this.unhighlight(object);
    116      this.update();
    117      this.highlight(
    118        object,
    119        { duration: VALUE_HIGHLIGHT_DURATION },
    120        scrollContentNodeIntoView
    121      );
    122    }
    123 
    124    if (this.props.highlighted) {
    125      this.scrollIntoView();
    126    }
    127  }
    128 
    129  /**
    130   * Update accessible object details that are going to be rendered inside the
    131   * accessible panel sidebar.
    132   */
    133  componentDidUpdate(prevProps) {
    134    const {
    135      member: { selected, object },
    136      scrollContentNodeIntoView,
    137    } = this.props;
    138    // If row is selected, update corresponding accessible details.
    139    if (!prevProps.member.selected && selected) {
    140      this.unhighlight(object);
    141      this.update();
    142      this.highlight(
    143        object,
    144        { duration: VALUE_HIGHLIGHT_DURATION },
    145        scrollContentNodeIntoView
    146      );
    147    }
    148 
    149    if (this.props.highlighted) {
    150      this.scrollIntoView();
    151    }
    152 
    153    if (!selected && prevProps.member.value !== this.props.member.value) {
    154      this.flashValue();
    155    }
    156  }
    157 
    158  scrollIntoView() {
    159    const row = findDOMNode(this);
    160    // Row might not be rendered in the DOM tree if it is filtered out during
    161    // audit.
    162    if (!row) {
    163      return;
    164    }
    165 
    166    scrollIntoView(row);
    167  }
    168 
    169  update() {
    170    const {
    171      dispatch,
    172      member: { object },
    173    } = this.props;
    174    if (!object.actorID) {
    175      return;
    176    }
    177 
    178    dispatch(updateDetails(object));
    179    window.emit(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED, object);
    180  }
    181 
    182  flashValue() {
    183    const row = findDOMNode(this);
    184    // Row might not be rendered in the DOM tree if it is filtered out during
    185    // audit.
    186    if (!row) {
    187      return;
    188    }
    189 
    190    const value = row.querySelector(".objectBox");
    191 
    192    flashElementOn(value);
    193    if (this._flashMutationTimer) {
    194      clearTimeout(this._flashMutationTimer);
    195      this._flashMutationTimer = null;
    196    }
    197    this._flashMutationTimer = setTimeout(() => {
    198      flashElementOff(value);
    199    }, VALUE_FLASHING_DURATION);
    200  }
    201 
    202  /**
    203   * Scroll the node that corresponds to a current accessible object into view.
    204   *
    205   * @param   {object}
    206   *          Accessible front that is rendered for this node.
    207   *
    208   * @returns {Promise}
    209   *          Promise that resolves when the node is scrolled into view if
    210   *          possible.
    211   */
    212  async scrollNodeIntoViewIfNeeded(accessibleFront) {
    213    if (accessibleFront.isDestroyed()) {
    214      return;
    215    }
    216 
    217    const domWalker = (await accessibleFront.targetFront.getFront("inspector"))
    218      .walker;
    219    if (accessibleFront.isDestroyed()) {
    220      return;
    221    }
    222 
    223    const node = await domWalker.getNodeFromActor(accessibleFront.actorID, [
    224      "rawAccessible",
    225      "DOMNode",
    226    ]);
    227    if (!node) {
    228      return;
    229    }
    230 
    231    if (node.nodeType == nodeConstants.ELEMENT_NODE) {
    232      await node.scrollIntoView();
    233    } else if (node.nodeType != nodeConstants.DOCUMENT_NODE) {
    234      // scrollIntoView method is only part of the Element interface, in cases
    235      // where node is a text node (and not a document node) scroll into view
    236      // its parent.
    237      await node.parentNode().scrollIntoView();
    238    }
    239  }
    240 
    241  async highlight(accessibleFront, options, scrollContentNodeIntoView) {
    242    this.props.dispatch(unhighlight());
    243    // If necessary scroll the node into view before showing the accessibility
    244    // highlighter.
    245    if (scrollContentNodeIntoView) {
    246      await this.scrollNodeIntoViewIfNeeded(accessibleFront);
    247    }
    248 
    249    this.props.highlightAccessible(accessibleFront, options);
    250  }
    251 
    252  unhighlight(accessibleFront) {
    253    this.props.dispatch(unhighlight());
    254    this.props.unhighlightAccessible(accessibleFront);
    255  }
    256 
    257  async printToJSON() {
    258    Glean.devtoolsAccessibility.accessibleContextMenuItemActivated[
    259      "print-to-json"
    260    ].add(1);
    261 
    262    const snapshot = await this.props.member.object.snapshot();
    263    openDocLink(
    264      `${JSON_URL_PREFIX}${encodeURIComponent(JSON.stringify(snapshot))}`
    265    );
    266  }
    267 
    268  onContextMenu(e) {
    269    e.stopPropagation();
    270    e.preventDefault();
    271 
    272    if (!this.props.toolboxDoc) {
    273      return;
    274    }
    275 
    276    const menu = new Menu({ id: "accessibility-row-contextmenu" });
    277    menu.append(
    278      new MenuItem({
    279        id: "menu-printtojson",
    280        label: L10N.getStr("accessibility.tree.menu.printToJSON"),
    281        click: () => this.printToJSON(),
    282      })
    283    );
    284 
    285    menu.popup(e.screenX, e.screenY, this.props.toolboxDoc);
    286 
    287    Glean.devtoolsAccessibility.accessibleContextMenuOpened.add(1);
    288  }
    289 
    290  /**
    291   * Render accessible row component.
    292   *
    293   * @returns acecssible-row React component.
    294   */
    295  render() {
    296    const { member } = this.props;
    297    const props = {
    298      ...this.props,
    299      onContextMenu: e => this.onContextMenu(e),
    300      onMouseOver: () => this.highlight(member.object),
    301      onMouseOut: () => this.unhighlight(member.object),
    302      key: `${member.path}-${member.active ? "active" : "inactive"}`,
    303    };
    304 
    305    return AuditController(
    306      {
    307        accessibleFront: member.object,
    308      },
    309      AuditFilter({}, HighlightableTreeRow(props))
    310    );
    311  }
    312 }
    313 
    314 const mapStateToProps = ({
    315  ui: { [PREFS.SCROLL_INTO_VIEW]: scrollContentNodeIntoView },
    316 }) => ({
    317  scrollContentNodeIntoView,
    318 });
    319 
    320 module.exports = connect(mapStateToProps, null, null, { withRef: true })(
    321  AccessibilityRow
    322 );