tor-browser

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

TreeView.mjs (23529B)


      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": ["open", "parent"] }] */
      6 
      7 import React from "resource://devtools/client/shared/vendor/react.mjs";
      8 import ReactDOM from "resource://devtools/client/shared/vendor/react-dom.mjs";
      9 import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
     10 import * as dom from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";
     11 
     12 import { ObjectProvider } from "resource://devtools/client/shared/components/tree/ObjectProvider.mjs";
     13 import TreeRowClass from "resource://devtools/client/shared/components/tree/TreeRow.mjs";
     14 import TreeHeaderClass from "resource://devtools/client/shared/components/tree/TreeHeader.mjs";
     15 
     16 import { scrollIntoView } from "resource://devtools/client/shared/scroll.mjs";
     17 
     18 const { cloneElement, Component, createFactory, createRef } = React;
     19 const { findDOMNode } = ReactDOM;
     20 
     21 const TreeRow = createFactory(TreeRowClass);
     22 const TreeHeader = createFactory(TreeHeaderClass);
     23 
     24 const SUPPORTED_KEYS = [
     25  "ArrowUp",
     26  "ArrowDown",
     27  "ArrowLeft",
     28  "ArrowRight",
     29  "End",
     30  "Home",
     31  "Enter",
     32  " ",
     33  "Escape",
     34 ];
     35 
     36 // Use a function in order to avoid shared Set/Array between each instance!
     37 function getDefaultProps() {
     38  return {
     39    object: null,
     40    renderRow: null,
     41    provider: ObjectProvider,
     42    expandedNodes: new Set(),
     43    selected: null,
     44    defaultSelectFirstNode: true,
     45    active: null,
     46    expandableStrings: true,
     47    bucketLargeArrays: false,
     48    maxStringLength: 50,
     49    columns: [],
     50  };
     51 }
     52 
     53 /**
     54 * This component represents a tree view with expandable/collapsible nodes.
     55 * The tree is rendered using <table> element where every node is represented
     56 * by <tr> element. The tree is one big table where nodes (rows) are properly
     57 * indented from the left to mimic hierarchical structure of the data.
     58 *
     59 * The tree can have arbitrary number of columns and so, might be use
     60 * as an expandable tree-table UI widget as well. By default, there is
     61 * one column for node label and one for node value.
     62 *
     63 * The tree is maintaining its (presentation) state, which consists
     64 * from list of expanded nodes and list of columns.
     65 *
     66 * Complete data provider interface:
     67 * var TreeProvider = {
     68 *   getChildren: function(object);
     69 *   hasChildren: function(object);
     70 *   getLabel: function(object, colId);
     71 *   getLevel: function(object); // optional
     72 *   getValue: function(object, colId);
     73 *   getKey: function(object);
     74 *   getType: function(object);
     75 * }
     76 *
     77 * Complete tree decorator interface:
     78 * var TreeDecorator = {
     79 *   getRowClass: function(object);
     80 *   getCellClass: function(object, colId);
     81 *   getHeaderClass: function(colId);
     82 *   renderValue: function(object, colId);
     83 *   renderRow: function(object);
     84 *   renderCell: function(object, colId);
     85 *   renderLabelCell: function(object);
     86 * }
     87 */
     88 class TreeView extends Component {
     89  // The only required property (not set by default) is the input data
     90  // object that is used to populate the tree.
     91  static get propTypes() {
     92    return {
     93      // The input data object.
     94      object: PropTypes.any,
     95      className: PropTypes.string,
     96      label: PropTypes.string,
     97      // Data provider (see also the interface above)
     98      provider: PropTypes.shape({
     99        getChildren: PropTypes.func,
    100        hasChildren: PropTypes.func,
    101        getLabel: PropTypes.func,
    102        getValue: PropTypes.func,
    103        getKey: PropTypes.func,
    104        getLevel: PropTypes.func,
    105        getType: PropTypes.func,
    106      }).isRequired,
    107      // Tree decorator (see also the interface above)
    108      decorator: PropTypes.shape({
    109        getRowClass: PropTypes.func,
    110        getCellClass: PropTypes.func,
    111        getHeaderClass: PropTypes.func,
    112        renderValue: PropTypes.func,
    113        renderRow: PropTypes.func,
    114        renderCell: PropTypes.func,
    115        renderLabelCell: PropTypes.func,
    116      }),
    117      // Custom tree row (node) renderer
    118      renderRow: PropTypes.func,
    119      // Custom cell renderer
    120      renderCell: PropTypes.func,
    121      // Custom value renderer
    122      renderValue: PropTypes.func,
    123      // Custom tree label (including a toggle button) renderer
    124      renderLabelCell: PropTypes.func,
    125      // Set of expanded nodes
    126      expandedNodes: PropTypes.object,
    127      // Selected node
    128      selected: PropTypes.string,
    129      // Select first node by default
    130      defaultSelectFirstNode: PropTypes.bool,
    131      // The currently active (keyboard) item, if any such item exists.
    132      active: PropTypes.string,
    133      // Custom filtering callback
    134      onFilter: PropTypes.func,
    135      // Custom sorting callback
    136      onSort: PropTypes.func,
    137      // Enable bucketing for large arrays
    138      bucketLargeArrays: PropTypes.bool,
    139      // Custom row click callback
    140      onClickRow: PropTypes.func,
    141      // Row context menu event handler
    142      onContextMenuRow: PropTypes.func,
    143      // Tree context menu event handler
    144      onContextMenuTree: PropTypes.func,
    145      // A header is displayed if set to true
    146      header: PropTypes.bool,
    147      // Long string is expandable by a toggle button
    148      expandableStrings: PropTypes.bool,
    149      // Length at which a string is considered long
    150      maxStringLength: PropTypes.number,
    151      // Array of columns
    152      columns: PropTypes.arrayOf(
    153        PropTypes.shape({
    154          id: PropTypes.string.isRequired,
    155          title: PropTypes.string,
    156          width: PropTypes.string,
    157        })
    158      ),
    159    };
    160  }
    161 
    162  static get defaultProps() {
    163    return getDefaultProps();
    164  }
    165 
    166  static subPath(path, subKey) {
    167    return path + "/" + String(subKey).replace(/[\\/]/g, "\\$&");
    168  }
    169 
    170  /**
    171   * Creates a set with the paths of the nodes that should be expanded by default
    172   * according to the passed options.
    173   *
    174   * @param {object} The root node of the tree.
    175   * @param {object} [optional] An object with the following optional parameters:
    176   *   - maxLevel: nodes nested deeper than this level won't be expanded.
    177   *   - maxNodes: maximum number of nodes that can be expanded. The traversal is
    178         breadth-first, so expanding nodes nearer to the root will be preferred.
    179         Sibling nodes will either be all expanded or none expanded.
    180   * }
    181   */
    182  static getExpandedNodes(
    183    rootObj,
    184    { maxLevel = Infinity, maxNodes = Infinity } = {}
    185  ) {
    186    const expandedNodes = new Set();
    187    const queue = [
    188      {
    189        object: rootObj,
    190        level: 1,
    191        path: "",
    192      },
    193    ];
    194    while (queue.length) {
    195      const { object, level, path } = queue.shift();
    196      if (Object(object) !== object) {
    197        continue;
    198      }
    199      const keys = Object.keys(object);
    200      if (expandedNodes.size + keys.length > maxNodes) {
    201        // Avoid having children half expanded.
    202        break;
    203      }
    204      for (const key of keys) {
    205        const nodePath = TreeView.subPath(path, key);
    206        expandedNodes.add(nodePath);
    207        if (level < maxLevel) {
    208          queue.push({
    209            object: object[key],
    210            level: level + 1,
    211            path: nodePath,
    212          });
    213        }
    214      }
    215    }
    216    return expandedNodes;
    217  }
    218 
    219  constructor(props) {
    220    super(props);
    221 
    222    this.state = {
    223      expandedNodes: props.expandedNodes,
    224      columns: ensureDefaultColumn(props.columns),
    225      selected: props.selected,
    226      active: props.active,
    227      lastSelectedIndex: props.defaultSelectFirstNode ? 0 : null,
    228      mouseDown: false,
    229    };
    230 
    231    this.treeRef = createRef();
    232 
    233    this.toggle = this.toggle.bind(this);
    234    this.isExpanded = this.isExpanded.bind(this);
    235    this.onFocus = this.onFocus.bind(this);
    236    this.onKeyDown = this.onKeyDown.bind(this);
    237    this.onClickRow = this.onClickRow.bind(this);
    238    this.getSelectedRow = this.getSelectedRow.bind(this);
    239    this.selectRow = this.selectRow.bind(this);
    240    this.activateRow = this.activateRow.bind(this);
    241    this.isSelected = this.isSelected.bind(this);
    242    this.onFilter = this.onFilter.bind(this);
    243    this.onSort = this.onSort.bind(this);
    244    this.getMembers = this.getMembers.bind(this);
    245    this.renderRows = this.renderRows.bind(this);
    246  }
    247 
    248  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
    249  UNSAFE_componentWillReceiveProps(nextProps) {
    250    const { expandedNodes, selected } = nextProps;
    251    const state = {
    252      expandedNodes,
    253      lastSelectedIndex: this.getSelectedRowIndex(),
    254    };
    255 
    256    if (selected) {
    257      state.selected = selected;
    258    }
    259 
    260    this.setState(Object.assign({}, this.state, state));
    261  }
    262 
    263  shouldComponentUpdate(nextProps, nextState) {
    264    const {
    265      expandedNodes,
    266      columns,
    267      selected,
    268      active,
    269      lastSelectedIndex,
    270      mouseDown,
    271    } = this.state;
    272 
    273    return (
    274      expandedNodes !== nextState.expandedNodes ||
    275      columns !== nextState.columns ||
    276      selected !== nextState.selected ||
    277      active !== nextState.active ||
    278      lastSelectedIndex !== nextState.lastSelectedIndex ||
    279      mouseDown === nextState.mouseDown
    280    );
    281  }
    282 
    283  componentDidUpdate() {
    284    const selected = this.getSelectedRow();
    285    if (selected || this.state.active) {
    286      return;
    287    }
    288 
    289    const rows = this.visibleRows;
    290    if (rows.length === 0) {
    291      return;
    292    }
    293 
    294    // Only select a row if there is a previous lastSelected Index
    295    // This mostly happens when the treeview is loaded the first time
    296    if (this.state.lastSelectedIndex !== null) {
    297      this.selectRow(
    298        rows[Math.min(this.state.lastSelectedIndex, rows.length - 1)],
    299        { alignTo: "top" }
    300      );
    301    }
    302  }
    303 
    304  /**
    305   * Get rows that are currently visible. Some rows can be filtered and made
    306   * invisible, in which case, when navigating around the tree we need to
    307   * ignore the ones that are not reachable by the user.
    308   */
    309  get visibleRows() {
    310    return this.rows.filter(row => {
    311      const rowEl = findDOMNode(row);
    312      return rowEl?.offsetParent;
    313    });
    314  }
    315 
    316  // Node expand/collapse
    317 
    318  toggle(nodePath) {
    319    const nodes = this.state.expandedNodes;
    320    if (this.isExpanded(nodePath)) {
    321      nodes.delete(nodePath);
    322    } else {
    323      nodes.add(nodePath);
    324    }
    325 
    326    // Compute new state and update the tree.
    327    this.setState(
    328      Object.assign({}, this.state, {
    329        expandedNodes: nodes,
    330      })
    331    );
    332  }
    333 
    334  isExpanded(nodePath) {
    335    return this.state.expandedNodes.has(nodePath);
    336  }
    337 
    338  // Event Handlers
    339 
    340  onFocus(_event) {
    341    if (this.state.mouseDown) {
    342      return;
    343    }
    344    // Set focus to the first element, if none is selected or activated
    345    // This is needed because keyboard navigation won't work without an element being selected
    346    this.componentDidUpdate();
    347  }
    348 
    349  // eslint-disable-next-line complexity
    350  onKeyDown(event) {
    351    const keyEligibleForFirstLetterNavigation = event.key.length === 1;
    352    if (
    353      (!SUPPORTED_KEYS.includes(event.key) &&
    354        !keyEligibleForFirstLetterNavigation) ||
    355      event.shiftKey ||
    356      event.ctrlKey ||
    357      event.metaKey ||
    358      event.altKey
    359    ) {
    360      return;
    361    }
    362 
    363    const row = this.getSelectedRow();
    364    if (!row) {
    365      return;
    366    }
    367 
    368    const rows = this.visibleRows;
    369    const index = rows.indexOf(row);
    370    const { hasChildren, open } = row.props.member;
    371 
    372    switch (event.key) {
    373      case "ArrowRight":
    374        if (hasChildren) {
    375          if (open) {
    376            const firstChildRow = this.rows
    377              .slice(index + 1)
    378              .find(r => r.props.member.level > row.props.member.level);
    379            if (firstChildRow) {
    380              this.selectRow(firstChildRow, { alignTo: "bottom" });
    381            }
    382          } else {
    383            this.toggle(this.state.selected);
    384          }
    385        }
    386        break;
    387      case "ArrowLeft":
    388        if (hasChildren && open) {
    389          this.toggle(this.state.selected);
    390        } else {
    391          const parentRow = rows
    392            .slice(0, index)
    393            .reverse()
    394            .find(r => r.props.member.level < row.props.member.level);
    395          if (parentRow) {
    396            this.selectRow(parentRow, { alignTo: "top" });
    397          }
    398        }
    399        break;
    400      case "ArrowDown": {
    401        const nextRow = rows[index + 1];
    402        if (nextRow) {
    403          this.selectRow(nextRow, { alignTo: "bottom" });
    404        }
    405        break;
    406      }
    407      case "ArrowUp": {
    408        const previousRow = rows[index - 1];
    409        if (previousRow) {
    410          this.selectRow(previousRow, { alignTo: "top" });
    411        }
    412        break;
    413      }
    414      case "Home": {
    415        const firstRow = rows[0];
    416 
    417        if (firstRow) {
    418          this.selectRow(firstRow, { alignTo: "top" });
    419        }
    420        break;
    421      }
    422      case "End": {
    423        const lastRow = rows[rows.length - 1];
    424        if (lastRow) {
    425          this.selectRow(lastRow, { alignTo: "bottom" });
    426        }
    427        break;
    428      }
    429      case "Enter":
    430      case " ":
    431        // On space or enter make selected row active. This means keyboard
    432        // focus handling is passed on to the tree row itself.
    433        if (this.treeRef.current === document.activeElement) {
    434          event.stopPropagation();
    435          event.preventDefault();
    436          if (this.state.active !== this.state.selected) {
    437            this.activateRow(this.state.selected);
    438          }
    439 
    440          return;
    441        }
    442        break;
    443      case "Escape":
    444        event.stopPropagation();
    445        if (this.state.active != null) {
    446          this.activateRow(null);
    447        }
    448        break;
    449    }
    450 
    451    if (keyEligibleForFirstLetterNavigation) {
    452      const next = rows
    453        .slice(index + 1)
    454        .find(r => r.props.member.name.startsWith(event.key));
    455      if (next) {
    456        this.selectRow(next, { alignTo: "bottom" });
    457      }
    458    }
    459 
    460    // Focus should always remain on the tree container itself.
    461    this.treeRef.current.focus();
    462    event.preventDefault();
    463  }
    464 
    465  onClickRow(nodePath, event) {
    466    const onClickRow = this.props.onClickRow;
    467    const row = this.visibleRows.find(r => r.props.member.path === nodePath);
    468 
    469    // Call custom click handler and bail out if it returns true.
    470    if (
    471      onClickRow &&
    472      onClickRow.call(this, nodePath, event, row.props.member)
    473    ) {
    474      return;
    475    }
    476 
    477    event.stopPropagation();
    478 
    479    const cell = event.target.closest("td");
    480    if (cell && cell.classList.contains("treeLabelCell")) {
    481      this.toggle(nodePath);
    482    }
    483 
    484    this.selectRow(row, { preventAutoScroll: true });
    485  }
    486 
    487  onContextMenu(member, event) {
    488    const onContextMenuRow = this.props.onContextMenuRow;
    489    if (onContextMenuRow) {
    490      onContextMenuRow.call(this, member, event);
    491    }
    492  }
    493 
    494  getSelectedRow() {
    495    const rows = this.visibleRows;
    496    if (!this.state.selected || rows.length === 0) {
    497      return null;
    498    }
    499    return rows.find(row => this.isSelected(row.props.member.path));
    500  }
    501 
    502  getSelectedRowIndex() {
    503    const row = this.getSelectedRow();
    504    if (!row) {
    505      return this.props.defaultSelectFirstNode ? 0 : null;
    506    }
    507 
    508    return this.visibleRows.indexOf(row);
    509  }
    510 
    511  _scrollIntoView(row, options = {}) {
    512    const treeEl = this.treeRef.current;
    513    if (!treeEl || !row) {
    514      return;
    515    }
    516 
    517    const { props: { member: { path } = {} } = {} } = row;
    518    if (!path) {
    519      return;
    520    }
    521 
    522    const element = treeEl.ownerDocument.getElementById(path);
    523    if (!element) {
    524      return;
    525    }
    526 
    527    scrollIntoView(element, { ...options });
    528  }
    529 
    530  selectRow(row, options = {}) {
    531    const { props: { member: { path } = {} } = {} } = row;
    532    if (this.isSelected(path)) {
    533      return;
    534    }
    535 
    536    if (this.state.active != null) {
    537      const treeEl = this.treeRef.current;
    538      if (treeEl && treeEl !== treeEl.ownerDocument.activeElement) {
    539        treeEl.focus();
    540      }
    541    }
    542 
    543    if (!options.preventAutoScroll) {
    544      this._scrollIntoView(row, options);
    545    }
    546 
    547    this.setState({
    548      ...this.state,
    549      selected: path,
    550      active: null,
    551    });
    552  }
    553 
    554  activateRow(active) {
    555    this.setState({
    556      ...this.state,
    557      active,
    558    });
    559  }
    560 
    561  isSelected(nodePath) {
    562    return nodePath === this.state.selected;
    563  }
    564 
    565  isActive(nodePath) {
    566    return nodePath === this.state.active;
    567  }
    568 
    569  // Filtering & Sorting
    570 
    571  /**
    572   * Filter out nodes that don't correspond to the current filter.
    573   *
    574   * @return {boolean} true if the node should be visible otherwise false.
    575   */
    576  onFilter(object) {
    577    const onFilter = this.props.onFilter;
    578    return onFilter ? onFilter(object) : true;
    579  }
    580 
    581  onSort(parent, children) {
    582    const onSort = this.props.onSort;
    583    return onSort ? onSort(parent, children) : children;
    584  }
    585 
    586  // Members
    587 
    588  /**
    589   * Return children node objects (so called 'members') for given
    590   * parent object.
    591   */
    592  getMembers(parent, level, path) {
    593    // Strings don't have children. Note that 'long' strings are using
    594    // the expander icon (+/-) to display the entire original value,
    595    // but there are no child items.
    596    if (typeof parent == "string") {
    597      return [];
    598    }
    599 
    600    const { expandableStrings, provider, bucketLargeArrays, maxStringLength } =
    601      this.props;
    602    let children = provider.getChildren(parent, { bucketLargeArrays }) || [];
    603 
    604    // If the return value is non-array, the children
    605    // are being loaded asynchronously.
    606    if (!Array.isArray(children)) {
    607      return children;
    608    }
    609 
    610    children = this.onSort(parent, children) || children;
    611 
    612    return children.map(child => {
    613      const key = provider.getKey(child);
    614      const nodePath = TreeView.subPath(path, key);
    615      const type = provider.getType(child);
    616      let hasChildren = provider.hasChildren(child);
    617 
    618      // Value with no column specified is used for optimization.
    619      // The row is re-rendered only if this value changes.
    620      // Value for actual column is get when a cell is rendered.
    621      const value = provider.getValue(child);
    622 
    623      if (expandableStrings && isLongString(value, maxStringLength)) {
    624        hasChildren = true;
    625      }
    626 
    627      // Return value is a 'member' object containing meta-data about
    628      // tree node. It describes node label, value, type, etc.
    629      return {
    630        // An object associated with this node.
    631        object: child,
    632        // A label for the child node
    633        name: provider.getLabel(child),
    634        // Data type of the child node (used for CSS customization)
    635        type,
    636        // Class attribute computed from the type.
    637        rowClass: "treeRow-" + type,
    638        // Level of the child within the hierarchy (top == 0)
    639        level: provider.getLevel ? provider.getLevel(child, level) : level,
    640        // True if this node has children.
    641        hasChildren,
    642        // Value associated with this node (as provided by the data provider)
    643        value,
    644        // True if the node is expanded.
    645        open: this.isExpanded(nodePath),
    646        // Node path
    647        path: nodePath,
    648        // True if the node is hidden (used for filtering)
    649        hidden: !this.onFilter(child),
    650        // True if the node is selected with keyboard
    651        selected: this.isSelected(nodePath),
    652        // True if the node is activated with keyboard
    653        active: this.isActive(nodePath),
    654      };
    655    });
    656  }
    657 
    658  /**
    659   * Render tree rows/nodes.
    660   */
    661  renderRows(parent, level = 0, path = "") {
    662    let rows = [];
    663    const decorator = this.props.decorator;
    664    let renderRow = this.props.renderRow || TreeRow;
    665 
    666    // Get children for given parent node, iterate over them and render
    667    // a row for every one. Use row template (a component) from properties.
    668    // If the return value is non-array, the children are being loaded
    669    // asynchronously.
    670    const members = this.getMembers(parent, level, path);
    671    if (!Array.isArray(members)) {
    672      return members;
    673    }
    674 
    675    members.forEach(member => {
    676      if (decorator?.renderRow) {
    677        renderRow = decorator.renderRow(member.object) || renderRow;
    678      }
    679 
    680      const props = Object.assign({}, this.props, {
    681        key: `${member.path}-${member.active ? "active" : "inactive"}`,
    682        member,
    683        columns: this.state.columns,
    684        id: member.path,
    685        ref: row => row && this.rows.push(row),
    686        onClick: this.onClickRow.bind(this, member.path),
    687        onContextMenu: this.onContextMenu.bind(this, member),
    688      });
    689 
    690      // Render single row.
    691      rows.push(renderRow(props));
    692 
    693      // If a child node is expanded render its rows too.
    694      if (member.hasChildren && member.open) {
    695        const childRows = this.renderRows(
    696          member.object,
    697          level + 1,
    698          member.path
    699        );
    700 
    701        // If children needs to be asynchronously fetched first,
    702        // set 'loading' property to the parent row. Otherwise
    703        // just append children rows to the array of all rows.
    704        if (!Array.isArray(childRows)) {
    705          const lastIndex = rows.length - 1;
    706          props.member.loading = true;
    707          rows[lastIndex] = cloneElement(rows[lastIndex], props);
    708        } else {
    709          rows = rows.concat(childRows);
    710        }
    711      }
    712    });
    713 
    714    return rows;
    715  }
    716 
    717  render() {
    718    const root = this.props.object;
    719    const classNames = ["treeTable"];
    720    this.rows = [];
    721 
    722    const { className, onContextMenuTree } = this.props;
    723    // Use custom class name from props.
    724    if (className) {
    725      classNames.push(...className.split(" "));
    726    }
    727 
    728    // Alright, let's render all tree rows. The tree is one big <table>.
    729    let rows = this.renderRows(root, 0, "");
    730 
    731    // This happens when the view needs to do initial asynchronous
    732    // fetch for the root object. The tree might provide a hook API
    733    // for rendering animated spinner (just like for tree nodes).
    734    if (!Array.isArray(rows)) {
    735      rows = [];
    736    }
    737 
    738    const props = Object.assign({}, this.props, {
    739      columns: this.state.columns,
    740    });
    741 
    742    return dom.table(
    743      {
    744        className: classNames.join(" "),
    745        role: "tree",
    746        ref: this.treeRef,
    747        tabIndex: 0,
    748        onFocus: this.onFocus,
    749        onKeyDown: this.onKeyDown,
    750        onContextMenu: onContextMenuTree && onContextMenuTree.bind(this),
    751        onMouseDown: () => this.setState({ mouseDown: true }),
    752        onMouseUp: () => this.setState({ mouseDown: false }),
    753        onClick: () => {
    754          // Focus should always remain on the tree container itself.
    755          this.treeRef.current.focus();
    756        },
    757        onBlur: event => {
    758          if (this.state.active != null) {
    759            const { relatedTarget } = event;
    760            if (!this.treeRef.current.contains(relatedTarget)) {
    761              this.activateRow(null);
    762            }
    763          }
    764        },
    765        "aria-label": this.props.label || "",
    766        "aria-activedescendant": this.state.selected,
    767        cellPadding: 0,
    768        cellSpacing: 0,
    769      },
    770      TreeHeader(props),
    771      dom.tbody(
    772        {
    773          role: "presentation",
    774          tabIndex: -1,
    775        },
    776        rows
    777      )
    778    );
    779  }
    780 }
    781 
    782 // Helpers
    783 
    784 /**
    785 * There should always be at least one column (the one with toggle buttons)
    786 * and this function ensures that it's true.
    787 */
    788 function ensureDefaultColumn(columns) {
    789  if (!columns) {
    790    columns = [];
    791  }
    792 
    793  const defaultColumn = columns.filter(col => col.id == "default");
    794  if (defaultColumn.length) {
    795    return columns;
    796  }
    797 
    798  // The default column is usually the first one.
    799  return [{ id: "default" }, ...columns];
    800 }
    801 
    802 function isLongString(value, maxStringLength) {
    803  return typeof value == "string" && value.length > maxStringLength;
    804 }
    805 
    806 // Exports from this module
    807 export default TreeView;