azure-devops-ui
Version:
React components for building web UI in Azure DevOps
376 lines (375 loc) • 23.4 kB
JavaScript
import { __assign, __extends } from "tslib";
import "../../CommonImports";
import "../../Core/core.css";
import "./ListBox.css";
import * as React from "react";
import { ObservableLike } from '../../Core/Observable';
import * as Utils_Accessibility from '../../Core/Util/Accessibility';
import { format } from '../../Core/Util/String';
import { Icon } from '../../Icon';
import { renderListCell } from '../../List';
import { ItemsObserver, Observer } from '../../Observer';
import * as Resources from '../../Resources.Dropdown';
import { Spinner, SpinnerSize } from '../../Spinner';
import { ColumnSelect, renderEmptyCell, SimpleTableCell, Table, TableRow } from '../../Table';
import { Tree, TreeRow } from '../../TreeEx';
import { css } from '../../Util';
import { DropdownSelection } from '../../Utilities/DropdownSelection';
import { ArrayItemProvider, getItemsValue } from '../../Utilities/Provider';
import { TreeItemProvider } from '../../Utilities/TreeItemProvider';
import { ListBoxItemType } from "./ListBox.Props";
export var DefaultListBoxWidth = -100;
var ListBox = /** @class */ (function (_super) {
__extends(ListBox, _super);
function ListBox(props) {
var _this = _super.call(this, props) || this;
_this.tabbableIndex = -1;
_this.positions = [];
_this.count = 0;
_this.table = React.createRef();
_this.tree = React.createRef();
_this.getItemWidth = function () {
var width = _this.props.width;
return _this.multiSelect && width && width > 0 ? width - 40 /* TODO: Remove this, 40 is only correct with default font-size */ : width;
};
_this.loadingChanged = function () {
if (ObservableLike.getValue(_this.props.loading)) {
Utils_Accessibility.announce(Resources.AnnounceLoadingItems);
}
else {
Utils_Accessibility.announce(Resources.AnnounceFinishedLoadingItems);
var itemCount = _this.props.items.length;
Utils_Accessibility.announce(format(itemCount > 0 ? format(Resources.AnnounceItemCount, itemCount) : Resources.NoFilterResults), true);
}
return true;
};
_this.searchingChanged = function () {
if (ObservableLike.getValue(_this.props.searching)) {
Utils_Accessibility.announce(Resources.Searching);
}
else if (ObservableLike.getValue(_this.props.searching) === false) {
var resultCount = _this.props.items.length;
Utils_Accessibility.announce(resultCount > 0 ? format(Resources.AnnounceFilterResultCount, resultCount) : Resources.NoFilterResults, true);
}
return true;
};
_this.onItemsChanged = function () {
var items = getListBoxItemsValue(_this.wrappedItems || _this.props.items);
_this.tabbableIndex = -1;
_this.positions = [];
_this.count = 0;
for (var _i = 0, items_1 = items; _i < items_1.length; _i++) {
var item = items_1[_i];
var itemValue = ObservableLike.getValue(item);
if (itemValue && !listBoxItemSelectable(itemValue)) {
_this.positions.push(-1);
}
else {
if (_this.tabbableIndex === -1 && _this.selection.selectable(_this.positions.length)) {
_this.tabbableIndex = _this.positions.length;
}
_this.positions.push(++_this.count);
}
}
return true;
};
_this.onActivate = function (event, tableRow) {
if (_this.props.onActivate) {
_this.props.onActivate(event, tableRow.data);
}
};
_this.onSelect = function (event, tableRow) {
if (_this.props.onSelect) {
_this.props.onSelect(event, tableRow.data);
}
};
_this.onTreeActivate = function (event, treeRow) {
var items = _this.getListBoxItems();
if (_this.props.onActivate && items) {
var treeItem_1 = treeRow.data.underlyingItem;
var item = items.find(function (item) { return item.id === treeItem_1.id; });
item && _this.props.onActivate(event, item);
}
};
_this.onTreeSelect = function (event, treeRow) {
var items = _this.getListBoxItems();
if (_this.props.onSelect && items) {
var treeItem_2 = treeRow.data.underlyingItem;
var item = items.find(function (item) { return item.id === treeItem_2.id; });
item && _this.props.onSelect(event, item);
}
};
_this.renderListBoxRow = function (index, item, details) {
var _a = _this.props, excludeFocusZone = _a.excludeFocusZone, excludeTabStop = _a.excludeTabStop;
var items = getListBoxItemsValue(_this.wrappedItems || _this.props.items);
var focusable = !excludeFocusZone && _this.selection.selectable(index);
var rowDetails = __assign(__assign({ tooltipProps: item.tooltipProps || { text: item.text, overflowOnly: true, overflowDetected: overflowDetected } }, details), { ariaLabel: item.ariaLabel, ariaDescribedBy: item.type !== ListBoxItemType.Divider && item.type !== ListBoxItemType.Header && item.groupId ? "header-".concat(item.groupId) : undefined, ariaPosInSet: _this.positions[index] >= 0 ? _this.positions[index] : null, ariaSetSize: _this.positions[index] >= 0 ? _this.count : null, excludeTabStop: excludeTabStop || _this.tabbableIndex !== index, excludeFocusZone: !focusable, id: item.id, singleClickActivation: false, role: item.type === ListBoxItemType.Header || item.type === ListBoxItemType.Divider ? "presentation" : "option" });
return (React.createElement(Observer, { key: item.id || index, item: item }, function () { return (React.createElement(TableRow, { key: item.id || index, index: index, details: rowDetails, className: css("bolt-list-box-row", item.type === ListBoxItemType.Header && "bolt-list-box-header-row", item.type === ListBoxItemType.Divider && "bolt-list-box-divider-row", item.type === ListBoxItemType.Loading && "bolt-list-box-loading-row", _this.multiSelect && "bolt-list-box-multi-select-row", item.type !== ListBoxItemType.Header && item.type !== ListBoxItemType.Divider && "cursor-pointer", item.disabled && "bolt-list-box-item-disabled") }, _this.columns.map(function (tableColumn, columnIndex) {
if (_this.multiSelect && columnIndex === 0) {
if (item.type === ListBoxItemType.Divider || item.type === ListBoxItemType.Loading) {
return null;
}
else if (item.type === ListBoxItemType.Header) {
return document.body.classList.contains('dropdown-list-component-enabled') ? (React.createElement("span", { className: "bolt-table-cell bolt-list-cell bolt-header-cell", "data-column-index": "0", role: "presentation" })) : renderEmptyCell(index, columnIndex);
}
}
if (document.body.classList.contains('dropdown-list-component-enabled')) {
tableColumn = __assign(__assign({}, tableColumn), { className: "dropdown-list" });
}
return tableColumn.renderCell(index, columnIndex, tableColumn, item);
}))); }));
};
_this.renderListBoxTreeRow = function (index, item, details) {
var _a = _this.props, excludeFocusZone = _a.excludeFocusZone, excludeTabStop = _a.excludeTabStop;
var focusable = !excludeFocusZone && _this.selection.selectable(index);
var data = item.underlyingItem.data;
var rowDetails = __assign(__assign({ tooltipProps: { text: item.underlyingItem.text, overflowOnly: true, overflowDetected: overflowDetected } }, details), { ariaPosInSet: _this.positions[index] >= 0 ? _this.positions[index] : null, ariaSetSize: _this.positions[index] >= 0 ? _this.count : null, excludeTabStop: excludeTabStop || _this.tabbableIndex !== index, excludeFocusZone: !focusable, id: item.underlyingItem.id, singleClickActivation: false, role: data.type === ListBoxItemType.Header || data.type === ListBoxItemType.Divider ? "row" : "treeitem" });
return (React.createElement(Observer, { key: "observer-".concat(item.underlyingItem.id || index), item: item }, function () { return (React.createElement(TreeRow, { key: "row-".concat(item.underlyingItem.id || index), index: index, details: rowDetails, className: css("bolt-list-box-row", data.type === ListBoxItemType.Header && "bolt-list-box-header-row", data.type === ListBoxItemType.Divider && "bolt-list-box-divider-row", data.type === ListBoxItemType.Loading && "bolt-list-box-loading-row", _this.multiSelect && "bolt-list-box-multi-select-row", data.type !== ListBoxItemType.Header && data.type !== ListBoxItemType.Divider && "cursor-pointer", data.disabled && "bolt-list-box-item-disabled") }, _this.columns.map(function (treeColumn, columnIndex) {
return treeColumn.renderCell(index, columnIndex, treeColumn, item);
}))); }));
};
_this.renderListBoxCell = function (rowIndex, columnIndex, tableColumn, tableItem) {
return renderListBoxCell(rowIndex, columnIndex, tableColumn, tableItem, _this.multiSelect);
};
var _a = _this.props, selection = _a.selection, renderItem = _a.renderItem, items = _a.items;
_this.selection = selection || new DropdownSelection();
_this.multiSelect = _this.props.enforceSingleSelect || _this.props.showTree ? false : _this.selection.multiSelect;
// trees get custom columns
if (!_this.props.columns) {
_this.columns = [];
if (_this.multiSelect && _this.props.showChecksColumn !== false) {
_this.columns.push(new ColumnSelect({ excludeFocusZone: true }));
}
else if (!_this.multiSelect && _this.props.showChecksColumn === true) {
_this.columns.push({
id: "column-check",
width: 24,
renderCell: function (rowIndex, columnIndex) { return (document.body.classList.contains('dropdown-list-component-enabled') ? (React.createElement("div", { className: "dropdown-list checkmark-icon", "data-column-index": columnIndex, key: "column-check", role: "presentation" },
React.createElement(Icon, { ariaHidden: "removed", className: css(!_this.selection.selected(rowIndex) && "invisible"), iconName: "CheckMark" }))) : (React.createElement(SimpleTableCell, { columnIndex: columnIndex, key: "column-check", role: "presentation" },
React.createElement(Icon, { className: css(!_this.selection.selected(rowIndex) && "invisible"), iconName: "CheckMark" })))); },
readonly: true
});
}
_this.columns.push({
id: "text",
width: _this.getItemWidth(),
renderCell: renderItem || _this.renderListBoxCell,
className: css("bolt-list-box-text", _this.multiSelect ? "bolt-list-box-text-multi-select" : "bolt-list-box-text-single-select"),
readonly: true
});
}
else {
_this.columns = _this.props.columns;
}
// string items are wrapped once here. Only use a string array in the simple case where the items are not changing.
_this.wrappedItems = wrapListBoxItems(items);
_this.onItemsChanged();
return _this;
}
ListBox.prototype.componentDidUpdate = function () {
if (this.props.didUpdate) {
this.props.didUpdate();
}
};
ListBox.prototype.render = function () {
var _this = this;
var _a = this.props, ariaLabel = _a.ariaLabel, className = _a.className, containerClassName = _a.containerClassName, enforceSingleSelect = _a.enforceSingleSelect, focuszoneProps = _a.focuszoneProps, getUnselectableRanges = _a.getUnselectableRanges, items = _a.items, loading = _a.loading, searching = _a.searching, searchResultsLoadingText = _a.searchResultsLoadingText, showItemsWhileSearching = _a.showItemsWhileSearching, width = _a.width;
var itemsObservable = { observableValue: items, filter: this.onItemsChanged };
var listBoxItems = this.getListBoxItems();
var itemProvider = listBoxItems
? this.props.showTree
? new TreeItemProvider(convertListBoxItemsToTreeItems(listBoxItems))
: new ArrayItemProvider(listBoxItems)
: items;
if (!this.props.columns) {
this.columns[this.columns.length - 1].width = this.getItemWidth();
}
return (React.createElement(ItemsObserver, { getUnselectableRanges: getUnselectableRanges, items: items, selection: this.selection },
React.createElement(Observer, { items: itemsObservable, loading: { observableValue: loading || false, filter: this.loadingChanged }, searching: { observableValue: searching || false, filter: this.searchingChanged } }, function (props) {
if (props.searching && !showItemsWhileSearching) {
return (React.createElement("div", { className: "bolt-list-box-loading", style: { width: width } },
React.createElement(Spinner, { size: SpinnerSize.medium, label: searchResultsLoadingText || Resources.Searching })));
}
if (_this.props.showTree) {
var treeProvider_1 = itemProvider;
return (React.createElement(Tree, { ariaLabel: ariaLabel || undefined, className: css(className, "bolt-list-box", "bolt-list-box-tree"), columns: _this.columns, containerClassName: containerClassName, focuszoneProps: focuszoneProps, itemProvider: treeProvider_1, onActivate: _this.onTreeActivate, onSelect: _this.onTreeSelect, onToggle: function (event, treeItem) {
if (event.target.className.includes("bolt-tree-expand-button")) {
treeProvider_1.toggle(treeItem.underlyingItem);
if (_this.props.onToggle) {
_this.props.onToggle(event, treeItem.underlyingItem.data);
}
}
}, ref: _this.tree, renderRow: _this.renderListBoxTreeRow, role: "listbox", scrollable: true, singleClickActivation: false, selection: _this.selection, showHeader: false, showLines: false }));
}
else {
return (React.createElement(Table, { ariaLabel: ariaLabel || undefined, className: css(className, "bolt-list-box"), columns: _this.columns, containerClassName: containerClassName, enforceSingleSelect: enforceSingleSelect, focuszoneProps: focuszoneProps, itemProvider: itemProvider, onActivate: _this.onActivate, onSelect: _this.onSelect, renderRow: _this.renderListBoxRow, ref: _this.table, role: "listbox", scrollable: true, singleClickActivation: false, selection: _this.selection, showHeader: false, showLines: false, spacerWidth: 0 }));
}
})));
};
ListBox.prototype.scrollIntoView = function (rowIndex, options) {
if (this.table.current) {
return this.table.current.scrollIntoView(rowIndex, options);
}
else if (this.tree.current) {
return this.tree.current.scrollIntoView(rowIndex, options);
}
};
/**
* Try to pull list box items out of props and variables.
* Returns undefined in case where IItemProvider was passed in.
*/
ListBox.prototype.getListBoxItems = function () {
var _a, _b;
return ((_b = (_a = this.wrappedItems) !== null && _a !== void 0 ? _a : (this.props.items && (Array.isArray(this.props.items) ? this.props.items : this.props.items.value))) !== null && _b !== void 0 ? _b : undefined);
};
ListBox.defaultProps = {
getUnselectableRanges: getUnselectableRanges,
width: DefaultListBoxWidth
};
return ListBox;
}(React.Component));
export { ListBox };
export function renderListBoxCell(rowIndex, columnIndex, tableColumn, tableItem, multiSelect) {
if (tableItem.render) {
return tableItem.render(rowIndex, columnIndex, tableColumn, tableItem);
}
if (tableItem.type === ListBoxItemType.Divider) {
return (React.createElement(SimpleTableCell, { className: css(tableColumn.className, tableItem.className, multiSelect && "bolt-list-box-divider-multi-select"), columnIndex: columnIndex, colspan: multiSelect ? 2 : 1, key: tableItem.id, role: "presentation", tableColumn: tableColumn },
React.createElement("div", { className: "bolt-list-box-divider flex-grow" })));
}
else if (tableItem.type === ListBoxItemType.Loading) {
return React.createElement(LoadingCell, { columnIndex: columnIndex, key: tableItem.id, tableColumn: tableColumn, tableItem: tableItem });
}
return (React.createElement(SimpleTableCell, { className: css(tableColumn.className, tableItem.className, tableItem.type === ListBoxItemType.Header && "bolt-list-box-header"), columnIndex: columnIndex, key: tableItem.id, role: "presentation", tableColumn: tableColumn },
React.createElement("div", { id: tableItem.type === ListBoxItemType.Header ? "header-".concat(tableItem.groupId) : undefined, "aria-label": tableItem.type === ListBoxItemType.Header && !document.body.classList.contains('dropdown-list-component-enabled') ? format(Resources.HeaderAriaLabel, tableItem.text) : undefined, className: "bolt-list-box-cell-container" }, tableItem && renderListCell(tableItem, false))));
}
function overflowDetected(anchorElement) {
var overflowElement = anchorElement.querySelector(".text-ellipsis");
if (overflowElement) {
return overflowElement.scrollWidth > Math.ceil(overflowElement.offsetWidth);
}
return false;
}
/**
* Retrieve a list of unselectable ranges based on a itemSelectable function.
* @param items the set of items
* @param itemSelectable A function that returns false when an items is not selectable.
* Defaults to checking that the item type is not header or divider.
*/
export function getUnselectableRanges(items, itemSelectable) {
if (itemSelectable === void 0) { itemSelectable = listBoxItemSelectable; }
var ranges = [];
var beginIndex = -1;
for (var index = 0; index < items.length; index++) {
if (!itemSelectable(items[index]) && beginIndex < 0) {
beginIndex = index;
}
else if (itemSelectable(items[index]) && beginIndex >= 0) {
ranges.push({ beginIndex: beginIndex, endIndex: index - 1 });
beginIndex = -1;
}
}
if (beginIndex >= 0) {
ranges.push({ beginIndex: beginIndex, endIndex: items.length - 1 });
}
return ranges;
}
/**
* Return whether a ListBoxItem can be selected or not.
* @param item the ListBoxItem to evaluate
*/
export function listBoxItemSelectable(item) {
return item.type !== ListBoxItemType.Header && item.type !== ListBoxItemType.Divider && item.type !== ListBoxItemType.Loading && !item.disabled;
}
/**
* When items is a string[], wrap each item in a ListBoxItem. Otherwise, do nothing.
* @param items the items prop
*/
export function wrapListBoxItems(items) {
if (Array.isArray(items) && items.length && typeof items[0] === "string") {
return items.map(function (item) {
return { id: item, text: item };
});
}
}
/**
* Walk through the ListBoxItems and construct a tree
*/
export function convertListBoxItemsToTreeItems(items) {
var rootItems = [];
var itemsMap = new Map();
for (var _i = 0, items_2 = items; _i < items_2.length; _i++) {
var item = items_2[_i];
// include children that may have been processed before this parent
var precreatedTreeItem = itemsMap.get(item.id);
var treeItem = {
childItems: precreatedTreeItem === null || precreatedTreeItem === void 0 ? void 0 : precreatedTreeItem.childItems,
expanded: item.expanded,
data: item,
id: item.id,
text: item.text
};
if (!item.parent) {
rootItems.push(treeItem);
}
else {
var parent_1 = itemsMap.get(item.parent.id);
if (parent_1) {
if (parent_1.childItems) {
parent_1.childItems.push(treeItem);
}
else {
parent_1.childItems = [treeItem];
}
}
else {
// add a placeholder that tracks children until parent actually gets processed
itemsMap.set(item.parent.id, { childItems: [treeItem] });
}
}
itemsMap.set(treeItem.id, treeItem);
}
return rootItems;
}
/**
* Helper to get the value of the items prop. If items is a string[], it should first be wrapped using wrapListBoxItems.
* If it's an itemProvider, .value will be called on the provider.
* @param items the items prop. If items was provided as a string[], it should first be wrapped using wrapListBoxItems.
*/
export function getListBoxItemsValue(items) {
if (false) {
if (Array.isArray(items) && items.length && typeof items[0] === "string") {
console.warn("a string[] was passed for items and not wrapped first. Call wrapListBoxItems on items and pass in the results as items.");
}
}
return getItemsValue(items);
}
var LoadingCell = /** @class */ (function (_super) {
__extends(LoadingCell, _super);
function LoadingCell() {
return _super !== null && _super.apply(this, arguments) || this;
}
LoadingCell.prototype.componentDidMount = function () {
if (this.props.onMount) {
this.props.onMount();
}
};
LoadingCell.prototype.render = function () {
var _a = this.props, columnIndex = _a.columnIndex, tableColumn = _a.tableColumn, tableItem = _a.tableItem;
return (React.createElement(SimpleTableCell, { className: css(tableColumn.className, tableItem.className), columnIndex: columnIndex, colspan: 2, contentClassName: "justify-center", key: columnIndex, tableColumn: tableColumn },
React.createElement("div", { className: "bolt-list-box-loading" },
React.createElement(Spinner, { size: SpinnerSize.medium, label: Resources.Loading }))));
};
return LoadingCell;
}(React.Component));
export { LoadingCell };
export function isListBoxItemVisible(item) {
var parent = item.parent;
while (parent) {
if (!parent.expanded) {
return false;
}
parent = parent.parent;
}
return true;
}