tor-browser

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

Tree.js (30574B)


      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 React = require("resource://devtools/client/shared/vendor/react.mjs");
      8 const { Component, createFactory } = React;
      9 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     10 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     11 
     12 const { scrollIntoView } = ChromeUtils.importESModule(
     13  "resource://devtools/client/shared/scroll.mjs"
     14 );
     15 
     16 // Localized strings for (devtools/client/locales/en-US/components.properties)
     17 loader.lazyGetter(this, "L10N_COMPONENTS", function () {
     18  const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     19  return new LocalizationHelper(
     20    "devtools/client/locales/components.properties"
     21  );
     22 });
     23 
     24 loader.lazyGetter(this, "EXPAND_LABEL", function () {
     25  return L10N_COMPONENTS.getStr("treeNode.expandButtonTitle");
     26 });
     27 
     28 loader.lazyGetter(this, "COLLAPSE_LABEL", function () {
     29  return L10N_COMPONENTS.getStr("treeNode.collapseButtonTitle");
     30 });
     31 
     32 // depth
     33 const AUTO_EXPAND_DEPTH = 0;
     34 
     35 // Simplied selector targetting elements that can receive the focus,
     36 // full version at https://stackoverflow.com/questions/1599660.
     37 const FOCUSABLE_SELECTOR = [
     38  "a[href]:not([tabindex='-1'])",
     39  "button:not([disabled], [tabindex='-1'])",
     40  "iframe:not([tabindex='-1'])",
     41  "input:not([disabled], [tabindex='-1'])",
     42  "select:not([disabled], [tabindex='-1'])",
     43  "textarea:not([disabled], [tabindex='-1'])",
     44  "[tabindex]:not([tabindex='-1'])",
     45 ].join(", ");
     46 
     47 /**
     48 * An arrow that displays whether its node is expanded (▼) or collapsed
     49 * (▶). When its node has no children, it is hidden.
     50 */
     51 class ArrowExpander extends Component {
     52  static get propTypes() {
     53    return {
     54      expanded: PropTypes.bool,
     55    };
     56  }
     57 
     58  shouldComponentUpdate(nextProps) {
     59    return this.props.expanded !== nextProps.expanded;
     60  }
     61 
     62  render() {
     63    const { expanded } = this.props;
     64 
     65    const classNames = ["theme-twisty"];
     66    const title = expanded ? COLLAPSE_LABEL : EXPAND_LABEL;
     67 
     68    if (expanded) {
     69      classNames.push("open");
     70    }
     71    return dom.button({
     72      className: classNames.join(" "),
     73      title,
     74    });
     75  }
     76 }
     77 
     78 const treeIndent = dom.span({ className: "tree-indent" }, "\u200B");
     79 const treeLastIndent = dom.span(
     80  { className: "tree-indent tree-last-indent" },
     81  "\u200B"
     82 );
     83 
     84 class TreeNode extends Component {
     85  static get propTypes() {
     86    return {
     87      id: PropTypes.any.isRequired,
     88      index: PropTypes.number.isRequired,
     89      depth: PropTypes.number.isRequired,
     90      focused: PropTypes.bool.isRequired,
     91      active: PropTypes.bool.isRequired,
     92      expanded: PropTypes.bool.isRequired,
     93      item: PropTypes.any.isRequired,
     94      isExpandable: PropTypes.bool.isRequired,
     95      onClick: PropTypes.func,
     96      shouldItemUpdate: PropTypes.func,
     97      renderItem: PropTypes.func.isRequired,
     98    };
     99  }
    100 
    101  constructor(props) {
    102    super(props);
    103 
    104    this.treeNodeRef = React.createRef();
    105 
    106    this._onKeyDown = this._onKeyDown.bind(this);
    107  }
    108 
    109  componentDidMount() {
    110    // Make sure that none of the focusable elements inside the tree node
    111    // container are tabbable if the tree node is not active. If the tree node
    112    // is active and focus is outside its container, focus on the first
    113    // focusable element inside.
    114    const elms = this.getFocusableElements();
    115    if (this.props.active) {
    116      const doc = this.treeNodeRef.current.ownerDocument;
    117      if (elms.length && !elms.includes(doc.activeElement)) {
    118        elms[0].focus();
    119      }
    120    } else {
    121      elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
    122    }
    123  }
    124 
    125  shouldComponentUpdate(nextProps) {
    126    return (
    127      this.props.item !== nextProps.item ||
    128      (this.props.shouldItemUpdate &&
    129        this.props.shouldItemUpdate(this.props.item, nextProps.item)) ||
    130      this.props.focused !== nextProps.focused ||
    131      this.props.expanded !== nextProps.expanded ||
    132      this.props.depth !== nextProps.depth
    133    );
    134  }
    135 
    136  /**
    137   * Get a list of all elements that are focusable with a keyboard inside the
    138   * tree node.
    139   */
    140  getFocusableElements() {
    141    return this.treeNodeRef.current
    142      ? Array.from(
    143          this.treeNodeRef.current.querySelectorAll(FOCUSABLE_SELECTOR)
    144        )
    145      : [];
    146  }
    147 
    148  /**
    149   * Wrap and move keyboard focus to first/last focusable element inside the
    150   * tree node to prevent the focus from escaping the tree node boundaries.
    151   * element).
    152   *
    153   * @param  {DOMNode} current  currently focused element
    154   * @param  {boolean} back     direction
    155   * @return {boolean}          true there is a newly focused element.
    156   */
    157  _wrapMoveFocus(current, back) {
    158    const elms = this.getFocusableElements();
    159    let next;
    160 
    161    if (elms.length === 0) {
    162      return false;
    163    }
    164 
    165    if (back) {
    166      if (elms.indexOf(current) === 0) {
    167        next = elms[elms.length - 1];
    168        next.focus();
    169      }
    170    } else if (elms.indexOf(current) === elms.length - 1) {
    171      next = elms[0];
    172      next.focus();
    173    }
    174 
    175    return !!next;
    176  }
    177 
    178  _onKeyDown(e) {
    179    const { target, key, shiftKey } = e;
    180 
    181    if (key !== "Tab") {
    182      return;
    183    }
    184 
    185    const focusMoved = this._wrapMoveFocus(target, shiftKey);
    186    if (focusMoved) {
    187      // Focus was moved to the begining/end of the list, so we need to prevent
    188      // the default focus change that would happen here.
    189      e.preventDefault();
    190    }
    191 
    192    e.stopPropagation();
    193  }
    194 
    195  render() {
    196    const {
    197      depth,
    198      id,
    199      item,
    200      focused,
    201      active,
    202      expanded,
    203      renderItem,
    204      isExpandable,
    205    } = this.props;
    206 
    207    const arrow = isExpandable
    208      ? ArrowExpanderFactory({
    209          item,
    210          expanded,
    211        })
    212      : null;
    213 
    214    let ariaExpanded;
    215    if (this.props.isExpandable) {
    216      ariaExpanded = false;
    217    }
    218    if (this.props.expanded) {
    219      ariaExpanded = true;
    220    }
    221 
    222    const indents = Array.from({ length: depth }, (_, i) => {
    223      if (i == depth - 1) {
    224        return treeLastIndent;
    225      }
    226      return treeIndent;
    227    });
    228 
    229    const items = indents.concat(
    230      renderItem(item, depth, focused, arrow, expanded)
    231    );
    232 
    233    return dom.div(
    234      {
    235        id,
    236        className: `tree-node${focused ? " focused" : ""}${
    237          active ? " active" : ""
    238        }`,
    239        onClick: this.props.onClick,
    240        onKeyDownCapture: active ? this._onKeyDown : null,
    241        role: "treeitem",
    242        ref: this.treeNodeRef,
    243        "aria-level": depth + 1,
    244        "aria-expanded": ariaExpanded,
    245        "data-expandable": this.props.isExpandable,
    246      },
    247      ...items
    248    );
    249  }
    250 }
    251 
    252 const ArrowExpanderFactory = createFactory(ArrowExpander);
    253 const TreeNodeFactory = createFactory(TreeNode);
    254 
    255 /**
    256 * Create a function that calls the given function `fn` only once per animation
    257 * frame.
    258 *
    259 * @param {Function} fn
    260 * @param {object} options: object that contains the following properties:
    261 *                      - {Function} getDocument: A function that return the document
    262 *                                                the component is rendered in.
    263 * @returns {Function}
    264 */
    265 function oncePerAnimationFrame(fn, { getDocument }) {
    266  let animationId = null;
    267  let argsToPass = null;
    268  return function (...args) {
    269    argsToPass = args;
    270    if (animationId !== null) {
    271      return;
    272    }
    273 
    274    const doc = getDocument();
    275    if (!doc) {
    276      return;
    277    }
    278 
    279    animationId = doc.defaultView.requestAnimationFrame(() => {
    280      fn.call(this, ...argsToPass);
    281      animationId = null;
    282      argsToPass = null;
    283    });
    284  };
    285 }
    286 
    287 /**
    288 * A generic tree component. See propTypes for the public API.
    289 *
    290 * This tree component doesn't make any assumptions about the structure of your
    291 * tree data. Whether children are computed on demand, or stored in an array in
    292 * the parent's `_children` property, it doesn't matter. We only require the
    293 * implementation of `getChildren`, `getRoots`, `getParent`, and `isExpanded`
    294 * functions.
    295 *
    296 * This tree component is well tested and reliable. See the tests in ./tests
    297 * and its usage in the memory panel in mozilla-central.
    298 *
    299 * This tree component doesn't make any assumptions about how to render items in
    300 * the tree. You provide a `renderItem` function, and this component will ensure
    301 * that only those items whose parents are expanded and which are visible in the
    302 * viewport are rendered. The `renderItem` function could render the items as a
    303 * "traditional" tree or as rows in a table or anything else. It doesn't
    304 * restrict you to only one certain kind of tree.
    305 *
    306 * The tree comes with basic styling for the indent, the arrow, as well as
    307 * hovered and focused styles which can be override in CSS.
    308 *
    309 * ### Example Usage
    310 *
    311 * Suppose we have some tree data where each item has this form:
    312 *
    313 *     {
    314 *       id: Number,
    315 *       label: String,
    316 *       parent: Item or null,
    317 *       children: Array of child items,
    318 *       expanded: bool,
    319 *     }
    320 *
    321 * Here is how we could render that data with this component:
    322 *
    323 *     class MyTree extends Component {
    324 *       static get propTypes() {
    325 *         // The root item of the tree, with the form described above.
    326 *         return {
    327 *           root: PropTypes.object.isRequired
    328 *         };
    329 *       },
    330 *
    331 *       render() {
    332 *         return Tree({
    333 *           getRoots: () => [this.props.root],
    334 *
    335 *           getParent: item => item.parent,
    336 *           getChildren: item => item.children,
    337 *           getKey: item => item.id,
    338 *           isExpanded: item => item.expanded,
    339 *
    340 *           renderItem: (item, depth, isFocused, arrow, isExpanded) => {
    341 *             let className = "my-tree-item";
    342 *             if (isFocused) {
    343 *               className += " focused";
    344 *             }
    345 *             return dom.div({
    346 *               className,
    347 *             },
    348 *               arrow,
    349 *               // And here is the label for this item.
    350 *               dom.span({ className: "my-tree-item-label" }, item.label)
    351 *             );
    352 *           },
    353 *
    354 *           onExpand: item => dispatchExpandActionToRedux(item),
    355 *           onCollapse: item => dispatchCollapseActionToRedux(item),
    356 *         });
    357 *       }
    358 *     }
    359 */
    360 class Tree extends Component {
    361  static get propTypes() {
    362    return {
    363      // Required props
    364 
    365      // A function to get an item's parent, or null if it is a root.
    366      //
    367      // Type: getParent(item: Item) -> Maybe<Item>
    368      //
    369      // Example:
    370      //
    371      //     // The parent of this item is stored in its `parent` property.
    372      //     getParent: item => item.parent
    373      getParent: PropTypes.func.isRequired,
    374 
    375      // A function to get an item's children.
    376      //
    377      // Type: getChildren(item: Item) -> [Item]
    378      //
    379      // Example:
    380      //
    381      //     // This item's children are stored in its `children` property.
    382      //     getChildren: item => item.children
    383      getChildren: PropTypes.func.isRequired,
    384 
    385      // A function to check if the tree node for the item should be updated.
    386      //
    387      // Type: shouldItemUpdate(prevItem: Item, nextItem: Item) -> Boolean
    388      //
    389      // Example:
    390      //
    391      //     // This item should be updated if it's type is a long string
    392      //     shouldItemUpdate: (prevItem, nextItem) =>
    393      //       nextItem.type === "longstring"
    394      shouldItemUpdate: PropTypes.func,
    395 
    396      // A function which takes an item and ArrowExpander component instance and
    397      // returns a component, or text, or anything else that React considers
    398      // renderable.
    399      //
    400      // Type: renderItem(item: Item,
    401      //                  depth: Number,
    402      //                  isFocused: Boolean,
    403      //                  arrow: ReactComponent,
    404      //                  isExpanded: Boolean) -> ReactRenderable
    405      //
    406      // Example:
    407      //
    408      //     renderItem: (item, depth, isFocused, arrow, isExpanded) => {
    409      //       let className = "my-tree-item";
    410      //       if (isFocused) {
    411      //         className += " focused";
    412      //       }
    413      //       return dom.div(
    414      //         {
    415      //           className,
    416      //           style: { marginLeft: depth * 10 + "px" }
    417      //         },
    418      //         arrow,
    419      //         dom.span({ className: "my-tree-item-label" }, item.label)
    420      //       );
    421      //     },
    422      renderItem: PropTypes.func.isRequired,
    423 
    424      // A function which returns the roots of the tree (forest).
    425      //
    426      // Type: getRoots() -> [Item]
    427      //
    428      // Example:
    429      //
    430      //     // In this case, we only have one top level, root item. You could
    431      //     // return multiple items if you have many top level items in your
    432      //     // tree.
    433      //     getRoots: () => [this.props.rootOfMyTree]
    434      getRoots: PropTypes.func.isRequired,
    435 
    436      // A function to get a unique key for the given item. This helps speed up
    437      // React's rendering a *TON*.
    438      //
    439      // Type: getKey(item: Item) -> String
    440      //
    441      // Example:
    442      //
    443      //     getKey: item => `my-tree-item-${item.uniqueId}`
    444      getKey: PropTypes.func.isRequired,
    445 
    446      // A function to get whether an item is expanded or not. If an item is not
    447      // expanded, then it must be collapsed.
    448      //
    449      // Type: isExpanded(item: Item) -> Boolean
    450      //
    451      // Example:
    452      //
    453      //     isExpanded: item => item.expanded,
    454      isExpanded: PropTypes.func.isRequired,
    455 
    456      // Optional props
    457 
    458      // The currently focused item, if any such item exists.
    459      focused: PropTypes.any,
    460 
    461      // Handle when a new item is focused.
    462      onFocus: PropTypes.func,
    463 
    464      // The depth to which we should automatically expand new items.
    465      autoExpandDepth: PropTypes.number,
    466      // Should auto expand all new items or just the new items under the first
    467      // root item.
    468      autoExpandAll: PropTypes.bool,
    469 
    470      // Auto expand a node only if number of its children
    471      // are less than autoExpandNodeChildrenLimit
    472      autoExpandNodeChildrenLimit: PropTypes.number,
    473 
    474      // Note: the two properties below are mutually exclusive. Only one of the
    475      // label properties is necessary.
    476      // ID of an element whose textual content serves as an accessible label
    477      // for a tree.
    478      labelledby: PropTypes.string,
    479      // Accessibility label for a tree widget.
    480      label: PropTypes.string,
    481 
    482      // Optional event handlers for when items are expanded or collapsed.
    483      // Useful for dispatching redux events and updating application state,
    484      // maybe lazily loading subtrees from a worker, etc.
    485      //
    486      // Type:
    487      //     onExpand(item: Item)
    488      //     onCollapse(item: Item)
    489      //
    490      // Example:
    491      //
    492      //     onExpand: item => dispatchExpandActionToRedux(item)
    493      onExpand: PropTypes.func,
    494      onCollapse: PropTypes.func,
    495      // The currently active (keyboard) item, if any such item exists.
    496      active: PropTypes.any,
    497      // Optional event handler called with the current focused node when the
    498      // Enter key is pressed. Can be useful to allow further keyboard actions
    499      // within the tree node.
    500      onActivate: PropTypes.func,
    501      isExpandable: PropTypes.func,
    502      // Additional classes to add to the root element.
    503      className: PropTypes.string,
    504      // style object to be applied to the root element.
    505      style: PropTypes.object,
    506      // Prevents blur when Tree loses focus
    507      preventBlur: PropTypes.bool,
    508      getInitiallyExpanded: PropTypes.func,
    509    };
    510  }
    511 
    512  static get defaultProps() {
    513    return {
    514      autoExpandDepth: AUTO_EXPAND_DEPTH,
    515      autoExpandAll: true,
    516    };
    517  }
    518 
    519  constructor(props) {
    520    super(props);
    521 
    522    this.state = {
    523      autoExpanded: new Set(),
    524    };
    525 
    526    this.treeRef = React.createRef();
    527 
    528    const opaf = fn =>
    529      oncePerAnimationFrame(fn, {
    530        getDocument: () =>
    531          this.treeRef.current && this.treeRef.current.ownerDocument,
    532      });
    533 
    534    this._onExpand = opaf(this._onExpand).bind(this);
    535    this._onCollapse = opaf(this._onCollapse).bind(this);
    536    this._focusPrevNode = opaf(this._focusPrevNode).bind(this);
    537    this._focusNextNode = opaf(this._focusNextNode).bind(this);
    538    this._focusParentNode = opaf(this._focusParentNode).bind(this);
    539    this._focusFirstNode = opaf(this._focusFirstNode).bind(this);
    540    this._focusLastNode = opaf(this._focusLastNode).bind(this);
    541 
    542    this._autoExpand = this._autoExpand.bind(this);
    543    this._preventArrowKeyScrolling = this._preventArrowKeyScrolling.bind(this);
    544    this._preventEvent = this._preventEvent.bind(this);
    545    this._dfs = this._dfs.bind(this);
    546    this._dfsFromRoots = this._dfsFromRoots.bind(this);
    547    this._focus = this._focus.bind(this);
    548    this._activate = this._activate.bind(this);
    549    this._scrollNodeIntoView = this._scrollNodeIntoView.bind(this);
    550    this._onBlur = this._onBlur.bind(this);
    551    this._onKeyDown = this._onKeyDown.bind(this);
    552    this._nodeIsExpandable = this._nodeIsExpandable.bind(this);
    553  }
    554 
    555  componentDidMount() {
    556    this._autoExpand();
    557    if (this.props.focused) {
    558      this._scrollNodeIntoView(this.props.focused);
    559    }
    560  }
    561 
    562  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
    563  UNSAFE_componentWillReceiveProps() {
    564    this._autoExpand();
    565  }
    566 
    567  componentDidUpdate(prevProps) {
    568    if (this.props.focused && prevProps.focused !== this.props.focused) {
    569      this._scrollNodeIntoView(this.props.focused);
    570    }
    571  }
    572 
    573  _autoExpand() {
    574    const {
    575      autoExpandDepth,
    576      autoExpandNodeChildrenLimit,
    577      getInitiallyExpanded,
    578    } = this.props;
    579 
    580    if (!autoExpandDepth && !getInitiallyExpanded) {
    581      return;
    582    }
    583 
    584    // Automatically expand the first autoExpandDepth levels for new items. Do
    585    // not use the usual DFS infrastructure because we don't want to ignore
    586    // collapsed nodes. Any initially expanded items will be expanded regardless
    587    // of how deep they are.
    588    const autoExpand = (item, currentDepth) => {
    589      const initial = getInitiallyExpanded && getInitiallyExpanded(item);
    590 
    591      if (!initial && currentDepth >= autoExpandDepth) {
    592        return;
    593      }
    594 
    595      const children = this.props.getChildren(item);
    596      if (
    597        !initial &&
    598        autoExpandNodeChildrenLimit &&
    599        children.length > autoExpandNodeChildrenLimit
    600      ) {
    601        return;
    602      }
    603 
    604      if (!this.state.autoExpanded.has(item)) {
    605        this.props.onExpand(item);
    606        this.state.autoExpanded.add(item);
    607      }
    608 
    609      const length = children.length;
    610      for (let i = 0; i < length; i++) {
    611        autoExpand(children[i], currentDepth + 1);
    612      }
    613    };
    614 
    615    const roots = this.props.getRoots();
    616    const length = roots.length;
    617    if (this.props.autoExpandAll) {
    618      for (let i = 0; i < length; i++) {
    619        autoExpand(roots[i], 0);
    620      }
    621    } else if (length != 0) {
    622      autoExpand(roots[0], 0);
    623 
    624      if (getInitiallyExpanded) {
    625        for (let i = 1; i < length; i++) {
    626          if (getInitiallyExpanded(roots[i])) {
    627            autoExpand(roots[i], 0);
    628          }
    629        }
    630      }
    631    }
    632  }
    633 
    634  _preventArrowKeyScrolling(e) {
    635    switch (e.key) {
    636      case "ArrowUp":
    637      case "ArrowDown":
    638      case "ArrowLeft":
    639      case "ArrowRight":
    640        this._preventEvent(e);
    641        break;
    642    }
    643  }
    644 
    645  _preventEvent(e) {
    646    e.preventDefault();
    647    e.stopPropagation();
    648    if (e.nativeEvent) {
    649      if (e.nativeEvent.preventDefault) {
    650        e.nativeEvent.preventDefault();
    651      }
    652      if (e.nativeEvent.stopPropagation) {
    653        e.nativeEvent.stopPropagation();
    654      }
    655    }
    656  }
    657 
    658  /**
    659   * Perform a pre-order depth-first search from item.
    660   */
    661  _dfs(item, maxDepth = Infinity, traversal = [], _depth = 0) {
    662    traversal.push({ item, depth: _depth });
    663 
    664    if (!this.props.isExpanded(item)) {
    665      return traversal;
    666    }
    667 
    668    const nextDepth = _depth + 1;
    669 
    670    if (nextDepth > maxDepth) {
    671      return traversal;
    672    }
    673 
    674    const children = this.props.getChildren(item);
    675    const length = children.length;
    676    for (let i = 0; i < length; i++) {
    677      this._dfs(children[i], maxDepth, traversal, nextDepth);
    678    }
    679 
    680    return traversal;
    681  }
    682 
    683  /**
    684   * Perform a pre-order depth-first search over the whole forest.
    685   */
    686  _dfsFromRoots(maxDepth = Infinity) {
    687    const traversal = [];
    688 
    689    const roots = this.props.getRoots();
    690    const length = roots.length;
    691    for (let i = 0; i < length; i++) {
    692      this._dfs(roots[i], maxDepth, traversal);
    693    }
    694 
    695    return traversal;
    696  }
    697 
    698  /**
    699   * Expands current row.
    700   *
    701   * @param {object} item
    702   * @param {boolean} expandAllChildren
    703   */
    704  _onExpand(item, expandAllChildren) {
    705    if (this.props.onExpand) {
    706      this.props.onExpand(item);
    707 
    708      if (expandAllChildren) {
    709        const children = this._dfs(item);
    710        const length = children.length;
    711        for (let i = 0; i < length; i++) {
    712          this.props.onExpand(children[i].item);
    713        }
    714      }
    715    }
    716  }
    717 
    718  /**
    719   * Collapses current row.
    720   *
    721   * @param {object} item
    722   */
    723  _onCollapse(item) {
    724    if (this.props.onCollapse) {
    725      this.props.onCollapse(item);
    726    }
    727  }
    728 
    729  /**
    730   * Sets the passed in item to be the focused item.
    731   *
    732   * @param {object | undefined} item
    733   *        The item to be focused, or undefined to focus no item.
    734   *
    735   * @param {object | undefined} options
    736   *        An options object which can contain:
    737   *          - alignTo: "up" or "down" to indicate if we should scroll the element
    738   *                 to the top or the bottom of the scrollable container when
    739   *                 the element is off canvas.
    740   *          - preventAutoScroll: boolean, avoid scrolling automatically
    741   */
    742  _focus(item, options = {}) {
    743    const { preventAutoScroll } = options;
    744    if (item && !preventAutoScroll) {
    745      this._scrollNodeIntoView(item, options);
    746    }
    747 
    748    if (this.props.active != undefined) {
    749      this._activate(undefined);
    750      const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
    751      if (this.treeRef.current !== doc.activeElement) {
    752        this.treeRef.current.focus();
    753      }
    754    }
    755 
    756    if (this.props.onFocus) {
    757      this.props.onFocus(item);
    758    }
    759  }
    760 
    761  /**
    762   * Sets the passed in item to be the active item.
    763   *
    764   * @param {object | undefined} item
    765   *        The item to be activated, or undefined to activate no item.
    766   */
    767  _activate(item) {
    768    if (this.props.onActivate) {
    769      this.props.onActivate(item);
    770    }
    771  }
    772 
    773  /**
    774   * Sets the passed in item to be the focused item.
    775   *
    776   * @param {object | undefined} item
    777   *        The item to be scrolled to.
    778   *
    779   * @param {object | undefined} options
    780   *        An options object which can contain:
    781   *          - alignTo: "up" or "down" to indicate if we should scroll the element
    782   *                 to the top or the bottom of the scrollable container when
    783   *                 the element is off canvas.
    784   */
    785  _scrollNodeIntoView(item, options = {}) {
    786    if (!item) {
    787      return;
    788    }
    789    const treeElement = this.treeRef.current;
    790    const doc = treeElement && treeElement.ownerDocument;
    791    const element = doc.getElementById(this.props.getKey(item));
    792 
    793    if (element) {
    794      scrollIntoView(element, options);
    795    }
    796  }
    797 
    798  /**
    799   * Sets the state to have no focused item.
    800   */
    801  _onBlur(e) {
    802    if (this.props.active != undefined) {
    803      const { relatedTarget } = e;
    804      if (!this.treeRef.current.contains(relatedTarget)) {
    805        this._activate(undefined);
    806      }
    807    } else if (!this.props.preventBlur) {
    808      this._focus(undefined);
    809    }
    810  }
    811 
    812  /**
    813   * Handles key down events in the tree's container.
    814   *
    815   * @param {Event} e
    816   */
    817  // eslint-disable-next-line complexity
    818  _onKeyDown(e) {
    819    if (this.props.focused == null) {
    820      return;
    821    }
    822 
    823    // Allow parent nodes to use navigation arrows with modifiers.
    824    if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
    825      return;
    826    }
    827 
    828    this._preventArrowKeyScrolling(e);
    829    const doc = this.treeRef.current && this.treeRef.current.ownerDocument;
    830 
    831    switch (e.key) {
    832      case "ArrowUp":
    833        this._focusPrevNode();
    834        return;
    835 
    836      case "ArrowDown":
    837        this._focusNextNode();
    838        return;
    839 
    840      case "ArrowLeft":
    841        if (
    842          this.props.isExpanded(this.props.focused) &&
    843          this._nodeIsExpandable(this.props.focused)
    844        ) {
    845          this._onCollapse(this.props.focused);
    846        } else {
    847          this._focusParentNode();
    848        }
    849        return;
    850 
    851      case "ArrowRight":
    852        if (
    853          this._nodeIsExpandable(this.props.focused) &&
    854          !this.props.isExpanded(this.props.focused)
    855        ) {
    856          this._onExpand(this.props.focused);
    857        } else {
    858          this._focusNextNode();
    859        }
    860        return;
    861 
    862      case "Home":
    863        this._focusFirstNode();
    864        return;
    865 
    866      case "End":
    867        this._focusLastNode();
    868        return;
    869 
    870      case "Enter":
    871      case " ":
    872        if (this.treeRef.current === doc.activeElement) {
    873          this._preventEvent(e);
    874          if (this.props.active !== this.props.focused) {
    875            this._activate(this.props.focused);
    876          }
    877        }
    878        return;
    879 
    880      case "Escape":
    881        this._preventEvent(e);
    882        if (this.props.active != undefined) {
    883          this._activate(undefined);
    884        }
    885 
    886        if (this.treeRef.current !== doc.activeElement) {
    887          this.treeRef.current.focus();
    888        }
    889    }
    890  }
    891 
    892  /**
    893   * Sets the previous node relative to the currently focused item, to focused.
    894   */
    895  _focusPrevNode() {
    896    // Start a depth first search and keep going until we reach the currently
    897    // focused node. Focus the previous node in the DFS, if it exists. If it
    898    // doesn't exist, we're at the first node already.
    899 
    900    let prev;
    901 
    902    const traversal = this._dfsFromRoots();
    903    const length = traversal.length;
    904    for (let i = 0; i < length; i++) {
    905      const item = traversal[i].item;
    906      if (item === this.props.focused) {
    907        break;
    908      }
    909      prev = item;
    910    }
    911    if (prev === undefined) {
    912      return;
    913    }
    914 
    915    this._focus(prev, { alignTo: "top" });
    916  }
    917 
    918  /**
    919   * Handles the down arrow key which will focus either the next child
    920   * or sibling row.
    921   */
    922  _focusNextNode() {
    923    // Start a depth first search and keep going until we reach the currently
    924    // focused node. Focus the next node in the DFS, if it exists. If it
    925    // doesn't exist, we're at the last node already.
    926    const traversal = this._dfsFromRoots();
    927    const length = traversal.length;
    928    let i = 0;
    929 
    930    while (i < length) {
    931      if (traversal[i].item === this.props.focused) {
    932        break;
    933      }
    934      i++;
    935    }
    936 
    937    if (i + 1 < traversal.length) {
    938      this._focus(traversal[i + 1].item, { alignTo: "bottom" });
    939    }
    940  }
    941 
    942  /**
    943   * Handles the left arrow key, going back up to the current rows'
    944   * parent row.
    945   */
    946  _focusParentNode() {
    947    const parent = this.props.getParent(this.props.focused);
    948    if (!parent) {
    949      this._focusPrevNode(this.props.focused);
    950      return;
    951    }
    952 
    953    this._focus(parent, { alignTo: "top" });
    954  }
    955 
    956  _focusFirstNode() {
    957    const traversal = this._dfsFromRoots();
    958    this._focus(traversal[0].item, { alignTo: "top" });
    959  }
    960 
    961  _focusLastNode() {
    962    const traversal = this._dfsFromRoots();
    963    const lastIndex = traversal.length - 1;
    964    this._focus(traversal[lastIndex].item, { alignTo: "bottom" });
    965  }
    966 
    967  _nodeIsExpandable(item) {
    968    return this.props.isExpandable
    969      ? this.props.isExpandable(item)
    970      : !!this.props.getChildren(item).length;
    971  }
    972 
    973  render() {
    974    const traversal = this._dfsFromRoots();
    975    const { active, focused } = this.props;
    976 
    977    const nodes = traversal.map((v, i) => {
    978      const { item, depth } = traversal[i];
    979      const key = this.props.getKey(item, i);
    980      const focusedKey = focused ? this.props.getKey(focused, i) : null;
    981      return TreeNodeFactory({
    982        // We make a key unique depending on whether the tree node is in active
    983        // or inactive state to make sure that it is actually replaced and the
    984        // tabbable state is reset.
    985        key: `${key}-${active === item ? "active" : "inactive"}`,
    986        id: key,
    987        index: i,
    988        item,
    989        depth,
    990        shouldItemUpdate: this.props.shouldItemUpdate,
    991        renderItem: this.props.renderItem,
    992        focused: focusedKey === key,
    993        active: active === item,
    994        expanded: this.props.isExpanded(item),
    995        isExpandable: this._nodeIsExpandable(item),
    996        onExpand: this._onExpand,
    997        onCollapse: this._onCollapse,
    998        onClick: e => {
    999          // We can stop the propagation since click handler on the node can be
   1000          // created in `renderItem`.
   1001          e.stopPropagation();
   1002 
   1003          // Since the user just clicked the node, there's no need to check if
   1004          // it should be scrolled into view.
   1005          this._focus(item, { preventAutoScroll: true });
   1006          if (this.props.isExpanded(item)) {
   1007            this.props.onCollapse(item, e.altKey);
   1008          } else {
   1009            this.props.onExpand(item, e.altKey);
   1010          }
   1011 
   1012          // Focus should always remain on the tree container itself.
   1013          this.treeRef.current.focus();
   1014        },
   1015      });
   1016    });
   1017 
   1018    const style = Object.assign({}, this.props.style || {});
   1019 
   1020    return dom.div(
   1021      {
   1022        className: `tree ${this.props.className ? this.props.className : ""}`,
   1023        ref: this.treeRef,
   1024        role: "tree",
   1025        tabIndex: "0",
   1026        onKeyDown: this._onKeyDown,
   1027        onKeyPress: this._preventArrowKeyScrolling,
   1028        onKeyUp: this._preventArrowKeyScrolling,
   1029        onFocus: ({ nativeEvent }) => {
   1030          if (focused || !nativeEvent || !this.treeRef.current) {
   1031            return;
   1032          }
   1033 
   1034          const { explicitOriginalTarget } = nativeEvent;
   1035          // Only set default focus to the first tree node if the focus came
   1036          // from outside the tree (e.g. by tabbing to the tree from other
   1037          // external elements).
   1038          if (
   1039            explicitOriginalTarget !== this.treeRef.current &&
   1040            !this.treeRef.current.contains(explicitOriginalTarget)
   1041          ) {
   1042            this._focus(traversal[0].item);
   1043          }
   1044        },
   1045        onBlur: this._onBlur,
   1046        "aria-label": this.props.label,
   1047        "aria-labelledby": this.props.labelledby,
   1048        "aria-activedescendant": focused && this.props.getKey(focused),
   1049        style,
   1050      },
   1051      nodes
   1052    );
   1053  }
   1054 }
   1055 
   1056 module.exports = Tree;