UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

267 lines (266 loc) 16.4 kB
import "../../CommonImports"; import "../../Core/core.css"; import "./Tree.css"; import "./TreeExpand.css"; import * as React from "react"; import { ObservableLike } from '../../Core/Observable'; import { FocusWithin } from '../../FocusWithin'; import { FocusZone, FocusZoneContext, FocusZoneDirection, FocusZoneKeyStroke } from '../../FocusZone'; import { getDefaultAnchorProps } from '../../Link'; import { renderListCell } from '../../List'; import { UncheckedObserver } from '../../Observer'; import { renderColumns, renderLoadingCell, SimpleTableCell, Table } from '../../Table'; import { css, getSafeId, KeyCode, preventDefault } from '../../Util'; import { getTabIndex } from '../../Utilities/Focus'; import { TreeExpand } from "./TreeExpand"; export class Tree extends React.Component { constructor() { super(...arguments); this.table = React.createRef(); this.onActivateExpand = (event, tableRow) => { if (!event.defaultPrevented && tableRow.data.underlyingItem.childItems) { this.props.onToggle && this.props.onToggle(event, tableRow.data); event.preventDefault(); } }; this.renderRow = (rowIndex, item, details) => { // If onActivate for tree is not specified but onToggle is, tree passes on onActivateExpand as onActivate to table. // In that case, onActivate can be different for tree and table details.singleClickActivation = this.props.onActivate && details.singleClickActivation; if (this.props.columns.length <= 1 && !details.role) { details.role = "treeitem"; } // Since the underlying table is unable to determine whether or not a row // is in a loading state since the observable value is within the ITreeItemEx // we need to handle this in the row rendering. return (React.createElement(UncheckedObserver, { data: item.underlyingItem.data, key: item.underlyingItem.id }, (props) => { if (props.data) { // We need to forward the onToggle handler to the treeItemEx before it is rendered. var canToggle = item.underlyingItem.childItems && item.underlyingItem.childItems.length !== 0; // edge case: if row is a pipeline folder, we just know childItems exist, but not the length const anyData = props.data; if (anyData.folder) { canToggle = item.underlyingItem.childItems !== undefined; } item.onToggle = canToggle ? this.props.onToggle : undefined; // First determine if the item supplied a custom row rendering function, if not // attempt to use the global row rendering function. const renderRow = item.underlyingItem.data.renderRow || this.props.renderRow; if (renderRow) { return renderRow(rowIndex, item, details); } return renderTreeRow(rowIndex, item, details, this.props.columns, props.data); } else { const { renderLoadingRow } = this.props; // If a custom row loading animation is available use it. if (renderLoadingRow) { return renderLoadingRow(rowIndex, details); } // Return the default row loading animation. return (React.createElement(TreeRow, { index: rowIndex, details: details }, this.props.columns.map((treeColumn, columnIndex) => { let children = renderLoadingCell(treeColumn.columnLayout); if (treeColumn.hierarchical) { children = React.createElement(TreeExpand, { depth: details.data.depth }, children); } return SimpleTableCell({ className: "bolt-tree-cell", columnIndex, children }); }))); } })); }; } render() { const { role = this.props.role ? this.props.role : this.props.columns.length > 1 ? "treegrid" : "tree" } = this.props; // If we haven't specified an onActivate, but have specified an onToggle, toggle on activate. const onActivate = this.props.onActivate ? this.props.onActivate : this.props.onToggle ? this.onActivateExpand : undefined; return (React.createElement(Table, { ariaLabel: this.props.ariaLabel, behaviors: this.props.behaviors, className: this.props.className, columns: this.props.columns, containerClassName: this.props.containerClassName, eventDispatch: this.props.eventDispatch, focuszoneProps: this.props.focuszoneProps, id: this.props.id, itemProvider: this.props.itemProvider, maxHeight: this.props.maxHeight, onActivate: onActivate, onFocus: this.props.onFocus, onSelect: this.props.onSelect, pageSize: this.props.pageSize, renderHeader: this.props.renderHeader, renderRow: this.renderRow, renderSpacer: this.props.renderSpacer, role: role, rowHeight: this.props.rowHeight, ref: this.table, scrollable: this.props.scrollable, selectableText: this.props.selectableText, selection: this.props.selection, singleClickActivation: this.props.singleClickActivation, showHeader: this.props.showHeader, showLines: this.props.showLines, showScroll: this.props.showScroll, tableBreakpoints: this.props.tableBreakpoints, virtualize: this.props.virtualize, excludeTabStop: this.props.excludeTabStop })); } addOverlay(id, rowIndex, render, zIndex = 0) { if (this.table.current) { return this.table.current.addOverlay(id, rowIndex, render, zIndex); } } removeOverlay(id) { if (this.table.current) { return this.table.current.removeOverlay(id); } } focusRow(rowIndex, direction = 1) { if (this.table.current) { return this.table.current.focusRow(rowIndex, direction); } else { return Promise.resolve(); } } getFocusIndex() { if (this.table.current) { return this.table.current.getFocusIndex(); } return -1; } getStats() { if (this.table.current) { return this.table.current.getStats(); } return { firstMaterialized: -1, firstRendered: -1, lastMaterialized: -1, lastRendered: -1 }; } scrollIntoView(rowIndex, options) { if (this.table.current) { return this.table.current.scrollIntoView(rowIndex, options); } } } export class TreeRow extends React.Component { constructor() { super(...arguments); this.rowElement = React.createRef(); this.onFocus = (event) => { this.props.details.onFocusItem(this.props.index, event); }; this.onKeyDown = (event) => { if (!event.defaultPrevented) { if (this.rowElement.current === event.nativeEvent.srcElement) { const { data } = this.props.details; if (data) { if (data.onToggle) { const { expanded } = data.underlyingItem; if ((event.which === KeyCode.rightArrow && !expanded) || (event.which === KeyCode.leftArrow && expanded)) { data.onToggle(event, data); event.preventDefault(); } } } } } }; this.onPostprocessKeyStroke = (event) => { var _a; if (event.defaultPrevented) return FocusZoneKeyStroke.IgnoreNone; if (event.which !== KeyCode.leftArrow) return FocusZoneKeyStroke.IgnoreNone; const currentElement = this.rowElement.current; if (event.nativeEvent.srcElement !== currentElement) { currentElement === null || currentElement === void 0 ? void 0 : currentElement.focus(); return FocusZoneKeyStroke.IgnoreNone; } const { data } = this.props.details; if (!data || !data.parentItem) return FocusZoneKeyStroke.IgnoreNone; let prevElement = currentElement === null || currentElement === void 0 ? void 0 : currentElement.previousElementSibling; let currentElementAriaLevel = currentElement === null || currentElement === void 0 ? void 0 : currentElement.getAttribute('aria-level'); let prevElementAriaLevel = prevElement === null || prevElement === void 0 ? void 0 : prevElement.getAttribute('aria-level'); while (prevElement && currentElement && currentElementAriaLevel && prevElementAriaLevel) { const currentLevel = Number.parseInt(currentElementAriaLevel); const prevElementLevel = Number.parseInt(prevElementAriaLevel); if (prevElementLevel < currentLevel) break; prevElement = prevElement.previousElementSibling; } if (data.parentItem.onToggle && (prevElement === null || prevElement === void 0 ? void 0 : prevElement.id)) { (_a = document.getElementById(prevElement.id)) === null || _a === void 0 ? void 0 : _a.focus(); data.parentItem.onToggle(event, data.parentItem); } event.preventDefault(); return FocusZoneKeyStroke.IgnoreNone; }; } render() { const { details, index, linkProps } = this.props; const { ariaRowOffset, data, excludeFocusZone, renderSpacer, selectableText, selection, singleClickActivation } = details; // If the row is being rendered as a link we will use an anchor, otherwise we will // use a standard table row. const RowType = linkProps ? "a" : "tr"; // Build the set of props needed from the link to forward on to the row element. const linkForwardProps = getDefaultAnchorProps(linkProps); return (React.createElement(FocusWithin, { onFocus: this.onFocus }, (focusStatus) => { return (React.createElement(FocusZoneContext.Consumer, null, rowContext => { var _a; return (React.createElement(FocusZone, { direction: FocusZoneDirection.Horizontal, postprocessKeyStroke: this.onPostprocessKeyStroke }, React.createElement(RowType, Object.assign({}, linkForwardProps, { "aria-busy": data === undefined, "aria-current": details.ariaCurrent ? details.ariaCurrent : undefined, "aria-expanded": // default to false if the item has children without an expanded value data && data.underlyingItem.childItems ? data.underlyingItem.expanded === undefined ? false : data.underlyingItem.expanded : undefined, "aria-labelledby": details.ariaLabelledBy, "aria-level": data ? data.depth + 1 : undefined, "aria-rowindex": (details === null || details === void 0 ? void 0 : details.role) === "treeitem" ? undefined : index + ariaRowOffset, "aria-selected": selection && selection.selected(index) ? true : undefined, className: css(this.props.className, "bolt-tree-row bolt-table-row bolt-list-row", index === 0 && "first-row", focusStatus.hasFocus && "focused", selection && selection.selected(index) && "selected", singleClickActivation && "single-click-activation", selectableText && "selectable-text", linkProps && "v-align-middle"), "data-focuszone": excludeFocusZone || (selection && !selection.selectable(index)) ? undefined : rowContext.focuszoneId, "data-row-index": index, id: getSafeId((_a = details.data.underlyingItem.id) !== null && _a !== void 0 ? _a : index.toString()), onBlur: focusStatus.onBlur, onFocus: focusStatus.onFocus, onKeyDown: this.onKeyDown, ref: this.rowElement, role: details.role || "row", tabIndex: getTabIndex(details) }), React.createElement("td", { key: "left-spacer", className: "bolt-table-cell-compact bolt-table-cell bolt-list-cell", role: "presentation" }, renderSpacer && renderSpacer(index, true)), this.props.children, React.createElement("td", { key: "right-spacer", className: "bolt-table-cell-compact bolt-table-cell bolt-list-cell", role: "presentation" }, renderSpacer && renderSpacer(index, false))))); })); })); } } export function renderTreeRow(rowIndex, item, details, columns, data, className, key) { return (React.createElement(TreeRow, { index: rowIndex, details: details, linkProps: data ? data.linkProps : undefined, className: className, key: key }, renderColumns(rowIndex, columns, item, details))); } /** * Standard cell renderer for a tree cell with expandable children. This will use the tree items * state to determine whether or not the row is expanded etc. */ export function ExpandableTreeCell(props) { const { colspan, columnIndex, contentClassName, treeItem, treeColumn, role } = props; const { depth, onToggle, underlyingItem } = treeItem; const { expanded } = underlyingItem; const children = (React.createElement(TreeExpand, { expanded: expanded, depth: depth, indentationSize: treeColumn && treeColumn.indentationSize, onClick: preventDefault, onToggle: onToggle ? event => onToggle(event, treeItem) : undefined }, props.children)); return SimpleTableCell({ children, className: css(props.className, "bolt-tree-cell"), colspan, columnIndex, contentClassName, tableColumn: treeColumn, role: role }); } export function renderExpandableTreeCell(rowIndex, columnIndex, treeColumn, treeItem, ariaRowIndex, role) { const { underlyingItem } = treeItem; const data = ObservableLike.getValue(underlyingItem.data); const treeCell = data && data[treeColumn.id]; // Do not include padding if the table cell has an href const hasLink = !!(treeCell && typeof treeCell !== "string" && typeof treeCell !== "number" && treeCell.href); return ExpandableTreeCell({ children: treeCell && renderListCell(treeCell), className: treeColumn.className, columnIndex, contentClassName: hasLink ? "bolt-table-cell-content-with-link" : undefined, treeItem, treeColumn, role }); } export function renderTreeCell(rowIndex, columnIndex, treeColumn, treeItem, ariaRowIndex, role) { const { underlyingItem } = treeItem; const data = ObservableLike.getValue(underlyingItem.data); const treeCell = data && data[treeColumn.id]; // Do not include padding if the table cell has an href const hasLink = !!(treeCell && typeof treeCell !== "string" && typeof treeCell !== "number" && treeCell.href); return SimpleTableCell({ className: treeColumn.className, children: treeCell && renderListCell(treeCell), columnIndex, contentClassName: hasLink ? "bolt-table-cell-content-with-link" : undefined, tableColumn: treeColumn, role: role }); } export function renderTreeCellWithClassName(rowIndex, columnIndex, treeColumn, treeItem, contentClassName) { const { underlyingItem } = treeItem; const data = ObservableLike.getValue(underlyingItem.data); const treeCell = data && data[treeColumn.id]; // Do not include padding if the table cell has an href const hasLink = !!(treeCell && typeof treeCell !== "string" && typeof treeCell !== "number" && treeCell.href); return SimpleTableCell({ className: treeColumn.className, children: treeCell && renderListCell(treeCell), columnIndex, contentClassName: css(contentClassName, hasLink ? "bolt-table-cell-content-with-link" : undefined), tableColumn: treeColumn }); }