diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/shared/components/tree/TreeRow.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/components/tree/TreeRow.js')
-rw-r--r-- | devtools/client/shared/components/tree/TreeRow.js | 304 |
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; +}); |