azure-devops-ui
Version:
React components for building web UI in Azure DevOps
267 lines (266 loc) • 16.4 kB
JavaScript
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
});
}