tor-browser

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

TreeRow.mjs (8627B)


      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 /* eslint no-shadow: ["error", { "allow": ["MutationObserver"] }] */
      6 
      7 import React from "resource://devtools/client/shared/vendor/react.mjs";
      8 import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
      9 import * as dom from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";
     10 import ReactDOM from "resource://devtools/client/shared/vendor/react-dom.mjs";
     11 
     12 import TreeCellClass from "resource://devtools/client/shared/components/tree/TreeCell.mjs";
     13 import LabelCellClass from "resource://devtools/client/shared/components/tree/LabelCell.mjs";
     14 
     15 import {
     16  wrapMoveFocus,
     17  getFocusableElements,
     18 } from "resource://devtools/client/shared/focus.mjs";
     19 
     20 const { tr } = dom;
     21 const { findDOMNode } = ReactDOM;
     22 const { Component, createFactory, createRef } = React;
     23 
     24 // Tree
     25 const TreeCell = createFactory(TreeCellClass);
     26 const LabelCell = createFactory(LabelCellClass);
     27 
     28 const UPDATE_ON_PROPS = [
     29  "name",
     30  "open",
     31  "value",
     32  "loading",
     33  "level",
     34  "selected",
     35  "active",
     36  "hasChildren",
     37 ];
     38 
     39 /**
     40 * This template represents a node in TreeView component. It's rendered
     41 * using <tr> element (the entire tree is one big <table>).
     42 */
     43 class TreeRow extends Component {
     44  // See TreeView component for more details about the props and
     45  // the 'member' object.
     46  static get propTypes() {
     47    return {
     48      member: PropTypes.shape({
     49        object: PropTypes.object,
     50        name: PropTypes.string,
     51        type: PropTypes.string.isRequired,
     52        rowClass: PropTypes.string.isRequired,
     53        level: PropTypes.number.isRequired,
     54        hasChildren: PropTypes.bool,
     55        value: PropTypes.any,
     56        open: PropTypes.bool.isRequired,
     57        path: PropTypes.string.isRequired,
     58        hidden: PropTypes.bool,
     59        selected: PropTypes.bool,
     60        active: PropTypes.bool,
     61        loading: PropTypes.bool,
     62      }),
     63      decorator: PropTypes.object,
     64      renderCell: PropTypes.func,
     65      renderLabelCell: PropTypes.func,
     66      columns: PropTypes.array.isRequired,
     67      id: PropTypes.string.isRequired,
     68      provider: PropTypes.object.isRequired,
     69      onClick: PropTypes.func.isRequired,
     70      onContextMenu: PropTypes.func,
     71      onMouseOver: PropTypes.func,
     72      onMouseOut: PropTypes.func,
     73    };
     74  }
     75 
     76  constructor(props) {
     77    super(props);
     78 
     79    this.treeRowRef = createRef();
     80 
     81    this.getRowClass = this.getRowClass.bind(this);
     82    this._onKeyDown = this._onKeyDown.bind(this);
     83  }
     84 
     85  componentDidMount() {
     86    this._setTabbableState();
     87 
     88    // Child components might add/remove new focusable elements, watch for the
     89    // additions/removals of descendant nodes and update focusable state.
     90    const win = this.treeRowRef.current.ownerDocument.defaultView;
     91    const { MutationObserver } = win;
     92    this.observer = new MutationObserver(() => {
     93      this._setTabbableState();
     94    });
     95    this.observer.observe(this.treeRowRef.current, {
     96      childList: true,
     97      subtree: true,
     98    });
     99  }
    100 
    101  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
    102  UNSAFE_componentWillReceiveProps(nextProps) {
    103    // I don't like accessing the underlying DOM elements directly,
    104    // but this optimization makes the filtering so damn fast!
    105    // The row doesn't have to be re-rendered, all we really need
    106    // to do is toggling a class name.
    107    // The important part is that DOM elements don't need to be
    108    // re-created when they should appear again.
    109    if (nextProps.member.hidden != this.props.member.hidden) {
    110      const row = findDOMNode(this);
    111      row.classList.toggle("hidden");
    112    }
    113  }
    114 
    115  /**
    116   * Optimize row rendering. If props are the same do not render.
    117   * This makes the rendering a lot faster!
    118   */
    119  shouldComponentUpdate(nextProps) {
    120    for (const prop of UPDATE_ON_PROPS) {
    121      if (nextProps.member[prop] !== this.props.member[prop]) {
    122        return true;
    123      }
    124    }
    125 
    126    return false;
    127  }
    128 
    129  componentWillUnmount() {
    130    this.observer.disconnect();
    131    this.observer = null;
    132  }
    133 
    134  /**
    135   * Makes sure that none of the focusable elements inside the row container
    136   * are tabbable if the row is not active. If the row is active and focus
    137   * is outside its container, focus on the first focusable element inside.
    138   */
    139  _setTabbableState() {
    140    const elms = getFocusableElements(this.treeRowRef.current);
    141    if (elms.length === 0) {
    142      return;
    143    }
    144 
    145    const { active } = this.props.member;
    146    if (!active) {
    147      elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
    148      return;
    149    }
    150 
    151    if (!elms.includes(document.activeElement)) {
    152      elms[0].focus();
    153    }
    154  }
    155 
    156  _onKeyDown(e) {
    157    const { target, key, shiftKey } = e;
    158 
    159    if (key !== "Tab") {
    160      return;
    161    }
    162 
    163    const focusMoved = !!wrapMoveFocus(
    164      getFocusableElements(this.treeRowRef.current),
    165      target,
    166      shiftKey
    167    );
    168    if (focusMoved) {
    169      // Focus was moved to the begining/end of the list, so we need to
    170      // prevent the default focus change that would happen here.
    171      e.preventDefault();
    172    }
    173 
    174    e.stopPropagation();
    175  }
    176 
    177  getRowClass(object) {
    178    const decorator = this.props.decorator;
    179    if (!decorator || !decorator.getRowClass) {
    180      return [];
    181    }
    182 
    183    // Decorator can return a simple string or array of strings.
    184    let classNames = decorator.getRowClass(object);
    185    if (!classNames) {
    186      return [];
    187    }
    188 
    189    if (typeof classNames == "string") {
    190      classNames = [classNames];
    191    }
    192 
    193    return classNames;
    194  }
    195 
    196  render() {
    197    const member = this.props.member;
    198    const decorator = this.props.decorator;
    199 
    200    const props = {
    201      id: this.props.id,
    202      ref: this.treeRowRef,
    203      role: "treeitem",
    204      "aria-level": member.level + 1,
    205      "aria-selected": !!member.selected,
    206      onClick: this.props.onClick,
    207      onContextMenu: this.props.onContextMenu,
    208      onKeyDownCapture: member.active ? this._onKeyDown : undefined,
    209      onMouseOver: this.props.onMouseOver,
    210      onMouseOut: this.props.onMouseOut,
    211    };
    212 
    213    // Compute class name list for the <tr> element.
    214    const classNames = this.getRowClass(member.object) || [];
    215    classNames.push("treeRow");
    216    classNames.push(member.type + "Row");
    217 
    218    if (member.hasChildren) {
    219      classNames.push("hasChildren");
    220 
    221      // There are 2 situations where hasChildren is true:
    222      // 1. it is an object with children. Only set aria-expanded in this situation
    223      // 2. It is a long string (> 50 chars) that can be expanded to fully display it
    224      if (member.type !== "string") {
    225        props["aria-expanded"] = member.open;
    226      }
    227    }
    228 
    229    if (member.open) {
    230      classNames.push("opened");
    231    }
    232 
    233    if (member.loading) {
    234      classNames.push("loading");
    235    }
    236 
    237    if (member.selected) {
    238      classNames.push("selected");
    239    }
    240 
    241    if (member.hidden) {
    242      classNames.push("hidden");
    243    }
    244 
    245    props.className = classNames.join(" ");
    246 
    247    // The label column (with toggle buttons) is usually
    248    // the first one, but there might be cases (like in
    249    // the Memory panel) where the toggling is done
    250    // in the last column.
    251    const cells = [];
    252 
    253    // Get components for rendering cells.
    254    let renderCell = this.props.renderCell || RenderCell;
    255    let renderLabelCell = this.props.renderLabelCell || RenderLabelCell;
    256    if (decorator?.renderLabelCell) {
    257      renderLabelCell =
    258        decorator.renderLabelCell(member.object) || renderLabelCell;
    259    }
    260 
    261    // Render a cell for every column.
    262    this.props.columns.forEach(col => {
    263      const cellProps = Object.assign({}, this.props, {
    264        key: col.id,
    265        id: col.id,
    266        value: this.props.provider.getValue(member.object, col.id),
    267      });
    268 
    269      if (decorator?.renderCell) {
    270        renderCell = decorator.renderCell(member.object, col.id);
    271      }
    272 
    273      const render = col.id == "default" ? renderLabelCell : renderCell;
    274 
    275      // Some cells don't have to be rendered. This happens when some
    276      // other cells span more columns. Note that the label cells contains
    277      // toggle buttons and should be usually there unless we are rendering
    278      // a simple non-expandable table.
    279      if (render) {
    280        cells.push(render(cellProps));
    281      }
    282    });
    283 
    284    // Render tree row
    285    return tr(props, cells);
    286  }
    287 }
    288 
    289 // Helpers
    290 
    291 const RenderCell = props => {
    292  return TreeCell(props);
    293 };
    294 
    295 const RenderLabelCell = props => {
    296  return LabelCell(props);
    297 };
    298 
    299 // Exports from this module
    300 export default TreeRow;