summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/tree/TreeRow.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/tree/TreeRow.js')
-rw-r--r--devtools/client/shared/components/tree/TreeRow.js304
1 files changed, 304 insertions, 0 deletions
diff --git a/devtools/client/shared/components/tree/TreeRow.js b/devtools/client/shared/components/tree/TreeRow.js
new file mode 100644
index 0000000000..23fe0227f7
--- /dev/null
+++ b/devtools/client/shared/components/tree/TreeRow.js
@@ -0,0 +1,304 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Make this available to both AMD and CJS environments
+define(function (require, exports, module) {
+ const {
+ Component,
+ createFactory,
+ createRef,
+ } = require("devtools/client/shared/vendor/react");
+ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+ const dom = require("devtools/client/shared/vendor/react-dom-factories");
+ const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+ const { tr } = dom;
+
+ // Tree
+ const TreeCell = createFactory(
+ require("devtools/client/shared/components/tree/TreeCell")
+ );
+ const LabelCell = createFactory(
+ require("devtools/client/shared/components/tree/LabelCell")
+ );
+
+ const {
+ wrapMoveFocus,
+ getFocusableElements,
+ } = require("devtools/client/shared/focus");
+
+ const UPDATE_ON_PROPS = [
+ "name",
+ "open",
+ "value",
+ "loading",
+ "level",
+ "selected",
+ "active",
+ "hasChildren",
+ ];
+
+ /**
+ * This template represents a node in TreeView component. It's rendered
+ * using <tr> element (the entire tree is one big <table>).
+ */
+ class TreeRow extends Component {
+ // See TreeView component for more details about the props and
+ // the 'member' object.
+ static get propTypes() {
+ return {
+ member: PropTypes.shape({
+ object: PropTypes.object,
+ name: PropTypes.string,
+ type: PropTypes.string.isRequired,
+ rowClass: PropTypes.string.isRequired,
+ level: PropTypes.number.isRequired,
+ hasChildren: PropTypes.bool,
+ value: PropTypes.any,
+ open: PropTypes.bool.isRequired,
+ path: PropTypes.string.isRequired,
+ hidden: PropTypes.bool,
+ selected: PropTypes.bool,
+ active: PropTypes.bool,
+ loading: PropTypes.bool,
+ }),
+ decorator: PropTypes.object,
+ renderCell: PropTypes.func,
+ renderLabelCell: PropTypes.func,
+ columns: PropTypes.array.isRequired,
+ id: PropTypes.string.isRequired,
+ provider: PropTypes.object.isRequired,
+ onClick: PropTypes.func.isRequired,
+ onContextMenu: PropTypes.func,
+ onMouseOver: PropTypes.func,
+ onMouseOut: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.treeRowRef = createRef();
+
+ this.getRowClass = this.getRowClass.bind(this);
+ this._onKeyDown = this._onKeyDown.bind(this);
+ }
+
+ componentDidMount() {
+ this._setTabbableState();
+
+ // Child components might add/remove new focusable elements, watch for the
+ // additions/removals of descendant nodes and update focusable state.
+ const win = this.treeRowRef.current.ownerDocument.defaultView;
+ const { MutationObserver } = win;
+ this.observer = new MutationObserver(() => {
+ this._setTabbableState();
+ });
+ this.observer.observe(this.treeRowRef.current, {
+ childList: true,
+ subtree: true,
+ });
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ // I don't like accessing the underlying DOM elements directly,
+ // but this optimization makes the filtering so damn fast!
+ // The row doesn't have to be re-rendered, all we really need
+ // to do is toggling a class name.
+ // The important part is that DOM elements don't need to be
+ // re-created when they should appear again.
+ if (nextProps.member.hidden != this.props.member.hidden) {
+ const row = findDOMNode(this);
+ row.classList.toggle("hidden");
+ }
+ }
+
+ /**
+ * Optimize row rendering. If props are the same do not render.
+ * This makes the rendering a lot faster!
+ */
+ shouldComponentUpdate(nextProps) {
+ for (const prop of UPDATE_ON_PROPS) {
+ if (nextProps.member[prop] !== this.props.member[prop]) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ componentWillUnmount() {
+ this.observer.disconnect();
+ this.observer = null;
+ }
+
+ /**
+ * Makes sure that none of the focusable elements inside the row container
+ * are tabbable if the row is not active. If the row is active and focus
+ * is outside its container, focus on the first focusable element inside.
+ */
+ _setTabbableState() {
+ const elms = getFocusableElements(this.treeRowRef.current);
+ if (elms.length === 0) {
+ return;
+ }
+
+ const { active } = this.props.member;
+ if (!active) {
+ elms.forEach(elm => elm.setAttribute("tabindex", "-1"));
+ return;
+ }
+
+ if (!elms.includes(document.activeElement)) {
+ elms[0].focus();
+ }
+ }
+
+ _onKeyDown(e) {
+ const { target, key, shiftKey } = e;
+
+ if (key !== "Tab") {
+ return;
+ }
+
+ const focusMoved = !!wrapMoveFocus(
+ getFocusableElements(this.treeRowRef.current),
+ target,
+ shiftKey
+ );
+ if (focusMoved) {
+ // Focus was moved to the begining/end of the list, so we need to
+ // prevent the default focus change that would happen here.
+ e.preventDefault();
+ }
+
+ e.stopPropagation();
+ }
+
+ getRowClass(object) {
+ const decorator = this.props.decorator;
+ if (!decorator || !decorator.getRowClass) {
+ return [];
+ }
+
+ // Decorator can return a simple string or array of strings.
+ let classNames = decorator.getRowClass(object);
+ if (!classNames) {
+ return [];
+ }
+
+ if (typeof classNames == "string") {
+ classNames = [classNames];
+ }
+
+ return classNames;
+ }
+
+ render() {
+ const member = this.props.member;
+ const decorator = this.props.decorator;
+
+ const props = {
+ id: this.props.id,
+ ref: this.treeRowRef,
+ role: "treeitem",
+ "aria-level": member.level + 1,
+ "aria-selected": !!member.selected,
+ onClick: this.props.onClick,
+ onContextMenu: this.props.onContextMenu,
+ onKeyDownCapture: member.active ? this._onKeyDown : undefined,
+ onMouseOver: this.props.onMouseOver,
+ onMouseOut: this.props.onMouseOut,
+ };
+
+ // Compute class name list for the <tr> element.
+ const classNames = this.getRowClass(member.object) || [];
+ classNames.push("treeRow");
+ classNames.push(member.type + "Row");
+
+ if (member.hasChildren) {
+ classNames.push("hasChildren");
+
+ // There are 2 situations where hasChildren is true:
+ // 1. it is an object with children. Only set aria-expanded in this situation
+ // 2. It is a long string (> 50 chars) that can be expanded to fully display it
+ if (member.type !== "string") {
+ props["aria-expanded"] = member.open;
+ }
+ }
+
+ if (member.open) {
+ classNames.push("opened");
+ }
+
+ if (member.loading) {
+ classNames.push("loading");
+ }
+
+ if (member.selected) {
+ classNames.push("selected");
+ }
+
+ if (member.hidden) {
+ classNames.push("hidden");
+ }
+
+ props.className = classNames.join(" ");
+
+ // The label column (with toggle buttons) is usually
+ // the first one, but there might be cases (like in
+ // the Memory panel) where the toggling is done
+ // in the last column.
+ const cells = [];
+
+ // Get components for rendering cells.
+ let renderCell = this.props.renderCell || RenderCell;
+ let renderLabelCell = this.props.renderLabelCell || RenderLabelCell;
+ if (decorator?.renderLabelCell) {
+ renderLabelCell =
+ decorator.renderLabelCell(member.object) || renderLabelCell;
+ }
+
+ // Render a cell for every column.
+ this.props.columns.forEach(col => {
+ const cellProps = Object.assign({}, this.props, {
+ key: col.id,
+ id: col.id,
+ value: this.props.provider.getValue(member.object, col.id),
+ });
+
+ if (decorator?.renderCell) {
+ renderCell = decorator.renderCell(member.object, col.id);
+ }
+
+ const render = col.id == "default" ? renderLabelCell : renderCell;
+
+ // Some cells don't have to be rendered. This happens when some
+ // other cells span more columns. Note that the label cells contains
+ // toggle buttons and should be usually there unless we are rendering
+ // a simple non-expandable table.
+ if (render) {
+ cells.push(render(cellProps));
+ }
+ });
+
+ // Render tree row
+ return tr(props, cells);
+ }
+ }
+
+ // Helpers
+
+ const RenderCell = props => {
+ return TreeCell(props);
+ };
+
+ const RenderLabelCell = props => {
+ return LabelCell(props);
+ };
+
+ // Exports from this module
+ module.exports = TreeRow;
+});