UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

842 lines (841 loc) 65 kB
import { __assign, __extends } from "tslib"; import "../../CommonImports"; import "../../Core/core.css"; import "./DropdownList.css"; import "./List.css"; import "./ListDropIndicator.css"; import * as React from "react"; import * as Utils_Accessibility from '../../Core/Util/Accessibility'; import { ObservableArray, ObservableLike } from '../../Core/Observable'; import { FocusWithin } from '../../FocusWithin'; import { FocusZone, FocusZoneContext, FocusZoneDirection } from '../../FocusZone'; import { Icon } from '../../Icon'; import { Intersection, IntersectionContext } from '../../Intersection'; import { getDefaultLinkProps, Link } from '../../Link'; import { Observer, UncheckedObserver } from '../../Observer'; import * as Resources from '../../Resources.Widgets'; import { Tooltip } from '../../TooltipEx'; import { css, eventTargetContainsNode, getSafeId, KeyCode } from '../../Util'; import { EventDispatch } from '../../Utilities/Dispatch'; import { getDragInProgress } from '../../Utilities/DragDrop'; import { getTabIndex } from '../../Utilities/Focus'; /** * The List component is used to render a collection of items with a series of rows. */ var List = /** @class */ (function (_super) { __extends(List, _super); function List(props) { var _this = _super.call(this, props) || this; // Track the table element used to render the rows. _this.bodyElement = React.createRef(); _this.listElement = React.createRef(); // Manage data about pages, including their spacers. _this.spacerElements = {}; _this.scrollToIndex = -1; _this.scrollToOptions = undefined; // Focus/Selection management members. _this.selectOnFocus = true; _this.focusIndex = -1; _this.pivotIndex = -1; _this.onVirtualizeKeyDown = function (e) { if (_this.state.virtualize && e.ctrlKey && e.altKey && e.key === "v") { var rowCount = _this.props.itemProvider.length; _this.setState({ virtualize: false, lastMaterialized: rowCount - 1, lastRendered: rowCount - 1, firstMaterialized: 0, firstRendered: 0 }); Utils_Accessibility.announce(Resources.VirtualizationDisabled); } }; _this.onBlur = function () { _this.focusIndex = -1; }; _this.onClick = function (event) { _this.onDispatch(event); if (!event.defaultPrevented && !(event.altKey && _this.props.selectableText)) { if (_this.listElement.current) { var _a = cellFromEvent(event), cellElement = _a.cellElement, rowIndex = _a.rowIndex; if (!cellElement || !eventTargetContainsNode(event, ["A"], cellElement)) { var item = ObservableLike.getValue(_this.state.rows[rowIndex]); if (rowIndex >= 0 && item) { var listRow = { data: item, index: rowIndex }; // Even for singleClickActivation we fire the selection before activation. if (_this.props.selectRowOnClick) { _this.processSelectionEvent(event, listRow); } // For singleClickActivation we want the activation as well. if (_this.props.singleClickActivation) { _this.rowActivated(event, listRow); } } } } } }; _this.onDispatch = function (event) { _this.state.eventDispatch.dispatchEvent(event); }; _this.onDoubleClick = function (event) { _this.onDispatch(event); if (!event.defaultPrevented && !_this.props.singleClickActivation) { var rowIndex = cellFromEvent(event).rowIndex; var item = ObservableLike.getValue(_this.state.rows[rowIndex]); if (rowIndex >= 0 && item) { _this.rowActivated(event, { data: item, index: rowIndex }); } } }; _this.onFocusBody = function (event) { // The first time the list gets focus we need to select initial row if we are performing // selection of focus. if (_this.selectOnFocus) { var selection = _this.props.selection; if (!selection || selection.selectOnFocus) { var rowIndex = _this.focusIndex; if (rowIndex >= 0) { var item = ObservableLike.getValue(_this.state.rows[rowIndex]); if (item) { _this.processSelectionEvent(event, { data: item, index: rowIndex }); } } } _this.selectOnFocus = false; } }; _this.onFocusItem = function (rowIndex, event) { var focusIndex = _this.focusIndex; if (focusIndex !== rowIndex) { // We need to re-render the previously focused row and newly focused row so we will // clear the cached values. if (focusIndex >= 0) { delete _this.state.renderedRows[focusIndex]; } else { // If there was a tabble row that was not the focusIndex row we need to update this // row as well to get it re-rendered without the tabIndex. delete _this.state.renderedRows[_this.getInitialTabbableRow()]; } delete _this.state.renderedRows[rowIndex]; _this.focusIndex = rowIndex; var item = ObservableLike.getValue(_this.state.rows[rowIndex]); if (item) { _this.rowFocused(event, { data: item, index: rowIndex }); } } }; _this.onKeyDown = function (event) { _this.onDispatch(event); if (!event.defaultPrevented) { var nodeName = event.target.nodeName; if (nodeName === "INPUT" || nodeName === "TEXTAREA") { // Don't handle keyboard events when target is an input return; } var focusIndex_1 = _this.focusIndex; var item = ObservableLike.getValue(_this.state.rows[focusIndex_1]); if (item) { if (event.which === KeyCode.enter) { if (focusIndex_1 >= 0 && !eventTargetContainsNode(event, ["A"])) { _this.rowActivated(event, { data: item, index: focusIndex_1 }); } } else if (event.which === KeyCode.space) { _this.processSelectionEvent(event, { data: item, index: focusIndex_1 }); event.preventDefault(); } else if (event.which === KeyCode.upArrow || event.which === KeyCode.downArrow) { var selection = _this.props.selection; if (!selection || (selection.selectOnFocus && (event.shiftKey || !event.ctrlKey))) { event.persist(); // Need to wait for the keyboard event to be processed by the focuszone. window.setTimeout(function () { if (_this.focusIndex != focusIndex_1) { var data = ObservableLike.getValue(_this.state.rows[_this.focusIndex]); if (data) { _this.processSelectionEvent(event, { data: data, index: _this.focusIndex }); } } }, 0); } } else if (event.which === KeyCode.pageDown) { var stats = _this.getStats(); _this.focusRow(Math.min(focusIndex_1 + (stats.lastRendered - stats.firstRendered), _this.state.rowCount - 1), 1); event.preventDefault(); } else if (event.which === KeyCode.pageUp) { var stats = _this.getStats(); _this.focusRow(Math.max(focusIndex_1 - (stats.lastRendered - stats.firstRendered), 0), -1); event.preventDefault(); } else if (event.which === KeyCode.home) { _this.focusRow(0, 1); event.preventDefault(); } else if (event.which === KeyCode.end) { _this.focusRow(_this.state.rowCount - 1, -1); event.preventDefault(); } } } }; _this.onIntersect = function (entries) { // If virtualization is disabled, we will not attempt to adjust the viewport. if (!_this.state.virtualize) { return; } var scrollTop = _this.context.root.scrollTop; var _a = _this.state, firstRendered = _a.firstRendered, firstMaterialized = _a.firstMaterialized, lastRendered = _a.lastRendered, lastMaterialized = _a.lastMaterialized, rowCount = _a.rowCount, rowProportion = _a.rowProportion; var rowHeight = _this.state.rowHeight; // console.log({ phase: "onIntersect - Start", firstMaterialized, lastMaterialized, rowHeight }); // Don't process an intersection while scroll event is pending. if (scrollTop !== _this.state.scrollTop && entries.length) { // console.log("Don't process an intersection while scroll event is pending."); return; } // Ignore events if we dont have a our basic elements resolved (this should never happen). if (!_this.listElement.current || !_this.bodyElement.current) { // console.log("Elements not available at this point."); return; } // We are going to enumerate all the children, if the row is in the viewport // we will determine if it should be paged out. var rowElements = _this.bodyElement.current.children; // If a rowHeight was specified we will compute one based on the average rowHeight in the // first page rendered. if (rowHeight === 0) { if (rowElements.length > 0) { var totalHeight = 0; var childCount = 0; // Loop through all children and average the rowHeight's. for (var childIndex = 0; childIndex < rowElements.length; childIndex++) { var child = _this.bodyElement.current.children[childIndex]; var childHeight = child.getBoundingClientRect().height; var isSpacer = child.classList.contains("bolt-list-row-spacer"); if (childHeight > 0 && !isSpacer) { totalHeight += childHeight; childCount++; } } // Make sure we have at least one child row that has size. if (childCount > 0) { rowHeight = totalHeight / childCount; } } if (rowHeight === 0) { return; } // If we have a pending scrollIntoView we will schedule it now that we have the rowHeight if (_this.scrollToIndex !== -1) { _this.setState({ firstMaterialized: Math.max(0, _this.scrollToIndex - _this.state.pageSize), lastMaterialized: _this.scrollToIndex + Math.min(_this.props.initialPageCount * _this.state.pageSize, rowCount - 1), rowHeight: rowHeight }); return; } // console.log({ phase: "onIntersect - Compute RowHeight", rowHeight }); } // Determine the location of the intersection within the page. This is the element // we are scrolling within. var intersectionRect = _this.context.root.getBoundingClientRect(); // Track the first and last row elements for adjusting the range. var firstMaterializedElement; var lastMaterializedElement; var firstMaterializedUpdated = firstMaterialized; var lastMaterializedUpdated = lastMaterialized; var firstRenderedUpdated = lastMaterializedUpdated; var lastRenderedUpdated = firstMaterializedUpdated; // Go through the viewport pages and determine if any are out of range and should be // paged out. Range is defined as more than 1 page of estimated rows away from the // nearest edge. If you dont allow for 1 page of estimated rows it may thrash pages // in and out of materialization. for (var childIndex = 0; childIndex < rowElements.length; childIndex++) { // Determine if this child is in the viewport, ignore rows that are not. var rowElement = rowElements[childIndex]; var rowIndex = getAttributeAsNumber(rowElement, "data-row-index"); var rowRect = rowElement.getBoundingClientRect(); if (rowIndex >= firstMaterialized && rowIndex <= lastMaterialized) { // Make sure to leave some extra room above and below the visible rectangle to handle // variable height rows. This helps prevent jittering when paging rows out. if (rowRect.bottom < intersectionRect.top - _this.state.pageSize * (rowProportion * rowHeight)) { firstMaterializedUpdated++; } else if (rowRect.top > intersectionRect.bottom + _this.state.pageSize * (rowProportion * rowHeight)) { lastMaterializedUpdated--; } // We will save the first and last rows for later computations. if (rowIndex === firstMaterialized) { firstMaterializedElement = rowElement; } if (rowIndex === lastMaterialized) { lastMaterializedElement = rowElement; } } // If the row is within the intersection rect, update the first and last rendered rows. These might be the focused items if (rowIndex > -1 && rowRect.top < intersectionRect.bottom && rowRect.bottom > intersectionRect.top) { lastRenderedUpdated = Math.max(lastRenderedUpdated, rowIndex); firstRenderedUpdated = Math.min(firstRenderedUpdated, rowIndex); } } // When we are scaling the size of the list, we want to keep a pageSize worth of elements materiaized but not rendered. // This allows users to scroll a few items. If they quickly scroll past the last materialized element or drag the scroll wheel, we recalculate where we should be // instead of paging in rows. if (rowProportion < 1) { if (firstMaterializedUpdated > lastMaterializedUpdated || firstRenderedUpdated === firstMaterializedUpdated || lastRenderedUpdated === lastMaterializedUpdated) { if (lastRenderedUpdated >= rowCount - 1) { firstMaterializedUpdated = Math.ceil(lastMaterializedUpdated - (intersectionRect.height / rowHeight + _this.state.pageSize)); } else { var offsetTop = scrollTop - (_this.listElement.current.offsetTop - _this.context.root.offsetTop); firstMaterializedUpdated = Math.max(0, Math.min(rowCount - 1, Math.floor(offsetTop / (rowProportion * rowHeight))) - _this.state.pageSize); lastMaterializedUpdated = Math.min(rowCount - 1, firstMaterializedUpdated + Math.ceil(intersectionRect.height / (rowProportion * rowHeight) + _this.state.pageSize - 1)); lastRenderedUpdated = -1; firstRenderedUpdated = -1; } } else { firstMaterializedUpdated = Math.min(firstMaterializedUpdated, firstRenderedUpdated - _this.state.pageSize); //-1 helps to avoid jittering when paging rows out lastMaterializedUpdated = Math.max(lastMaterializedUpdated, lastRenderedUpdated + _this.state.pageSize - 1); lastRenderedUpdated = -1; firstRenderedUpdated = -1; } } // If the row range is inverted (top above bottom) then all rows have been hidden and we should // recompute the viewport based on the scrollTop of our intersection and intersection height. else if (firstMaterializedUpdated > lastMaterializedUpdated) { var offsetTop = scrollTop - (_this.listElement.current.offsetTop - _this.context.root.offsetTop); var index = _this.props.rowHeights ? _this.getFirstMaterializedItemBaseOnRowHeights(_this.props.rowHeights, rowHeight, offsetTop) : undefined; if (index) { firstMaterializedUpdated = Math.max(0, Math.min(rowCount - 1, index - _this.state.pageSize)); } else { firstMaterializedUpdated = Math.max(0, Math.min(rowCount - 1, Math.floor(offsetTop / rowHeight)) - _this.state.pageSize); } lastMaterializedUpdated = Math.min(rowCount - 1, firstMaterializedUpdated + Math.ceil(intersectionRect.height / rowHeight + _this.state.pageSize - 1)); lastRenderedUpdated = -1; firstRenderedUpdated = -1; } else { // If the firstPage didn't move down, we may need more pages above. if (firstMaterializedUpdated === firstMaterialized && firstMaterializedElement) { var rowRect = firstMaterializedElement.getBoundingClientRect(); var availableSpace = rowRect.top - intersectionRect.top; if (availableSpace > 0) { firstMaterializedUpdated -= Math.ceil(availableSpace / rowHeight); } } // If the lastPage didn't move up, we may need more pages below. if (lastMaterializedUpdated === lastMaterialized && lastMaterializedElement) { var rowRect = lastMaterializedElement.getBoundingClientRect(); var availableSpace = intersectionRect.bottom - rowRect.bottom; if (availableSpace > 0) { lastMaterializedUpdated += Math.ceil(availableSpace / rowHeight); } } } // Make sure our page boundary stays in the available page range. firstMaterializedUpdated = Math.max(firstMaterializedUpdated, 0); lastMaterializedUpdated = Math.min(lastMaterializedUpdated, rowCount - 1); // console.log({ phase: "onIntersect - End", firstMaterializedUpdated, lastMaterializedUpdated, rowHeight }); // Update our state if and only if something has changed. if (firstMaterializedUpdated !== firstMaterialized || firstRenderedUpdated !== firstRendered || lastMaterializedUpdated !== lastMaterialized || lastRenderedUpdated !== lastRendered || rowHeight !== _this.state.rowHeight || scrollTop !== _this.state.scrollTop) { // // @TODO: We need to unload data for pages that are no longer rendererd. // This means not in the viewport or within any other rendered range. // // console.log({ phase: "onIntersect - stateChange", firstMaterializedUpdated, firstRenderedUpdated, lastRenderedUpdated, lastMaterializedUpdated, scrollTop }); _this.setState({ firstMaterialized: firstMaterializedUpdated, firstRendered: firstRenderedUpdated, lastMaterialized: lastMaterializedUpdated, lastRendered: lastRenderedUpdated, rowHeight: rowHeight, scrollTop: scrollTop }); } }; _this.onPointerDownBody = function (event) { // If the table body gets a mousedown, we will never need to fire the selection event when // the list gets focus since the mouse event will cause the selection. _this.selectOnFocus = false; }; _this.getInitialTabbableRow = function () { var _a = _this.props, defaultTabbableRow = _a.defaultTabbableRow, itemProvider = _a.itemProvider, selection = _a.selection; if (defaultTabbableRow) { return defaultTabbableRow; } if (selection) { for (var i = 0; i < itemProvider.length; i++) { if (selection.selectable(i)) { return i; } } } return 0; }; _this.getHeight = function (rowIndex, countFromBottom) { var height = 0; var rowHeights = _this.props.rowHeights || []; var start = countFromBottom ? _this.state.rowCount - rowIndex : 0; var end = countFromBottom ? _this.state.rowCount : rowIndex; for (var i = start; i < end; i++) { height += rowHeights[i] || _this.state.rowHeight; } return height; }; var rowCount = props.itemProvider.length; var pageSize = props.pageSize; _this.state = { columnCount: 1, eventDispatch: props.eventDispatch || new EventDispatch(), firstMaterialized: 0, firstRendered: 0, itemProvider: props.itemProvider, lastMaterialized: _this.props.virtualize ? Math.min(props.initialPageCount * pageSize, rowCount - 1) : rowCount - 1, lastRendered: _this.props.virtualize ? Math.min(props.initialPageCount * pageSize, rowCount - 1) : rowCount - 1, overlays: new ObservableArray(), pageSize: pageSize, renderedRows: {}, rowCount: rowCount, rowHeight: props.rowHeight || 0, rowProportion: props.rowHeight && props.maxHeight ? Math.min(1, props.maxHeight / (props.rowHeight * rowCount)) : 1, rows: {}, scrollTop: 0, virtualize: !!props.virtualize }; // Initialize the supplied behaviors. if (props.behaviors) { for (var _i = 0, _a = props.behaviors; _i < _a.length; _i++) { var behavior = _a[_i]; if (behavior.initialize) { behavior.initialize(props, _this, _this.state.eventDispatch); } } } return _this; } List.getDerivedStateFromProps = function (props, state) { var rowCount = props.itemProvider.length; var firstMaterialized = state.firstMaterialized; var lastMaterialized = state.lastMaterialized; if (rowCount !== state.rowCount) { firstMaterialized = Math.max(0, Math.min(state.firstMaterialized, rowCount)); lastMaterialized = state.virtualize ? Math.max(firstMaterialized, Math.min(state.lastMaterialized + (state.lastMaterialized === state.rowCount - 1 || state.lastMaterialized === state.rowCount ? props.pageSize : 0), rowCount - 1)) : rowCount - 1; } // Ensure out pages and providers are appropriately computed. var updatedState = { firstMaterialized: firstMaterialized, itemProvider: props.itemProvider, lastMaterialized: lastMaterialized, pageSize: props.pageSize, rowCount: rowCount, rowProportion: Math.min(1, (props.maxHeight || 100000) / (state.rowHeight * (rowCount - (lastMaterialized - firstMaterialized)))) }; // If there are changes to the props that affect the cached data, we need it clear it. if (props.itemProvider !== state.itemProvider || props.columnCount !== state.columnCount) { updatedState.columnCount = props.columnCount; updatedState.renderedRows = {}; updatedState.rows = {}; } // console.log(updatedState); return updatedState; }; List.prototype.render = function () { var _this = this; var _a = this.props, ariaRowOffset = _a.ariaRowOffset, className = _a.className, focuszoneProps = _a.focuszoneProps, id = _a.id, maxWidth = _a.maxWidth, minWidth = _a.minWidth, width = _a.width; var _b = this.state, firstMaterialized = _b.firstMaterialized, lastMaterialized = _b.lastMaterialized, rowCount = _b.rowCount, rowProportion = _b.rowProportion; var focusIndex = this.focusIndex; var role = this.props.role ? this.props.role : this.props.selection ? "listbox" : "list"; var useAriaCounts = role === "table" || role === "grid" || role === "treegrid"; var rows = []; // Number of pages each spacer takes up. There are potentially two spacers above // or below the view port. They surround the focus range when the focus range is // not within the viewport. var topSpacer1 = 0; var topSpacer2 = firstMaterialized; var bottomSpacer2 = Math.max(0, rowCount - lastMaterialized - 1); var bottomSpacer1 = 0; var firstFocusRow = Number.MAX_SAFE_INTEGER; var lastFocusRow = 0; // Compute the range of focus pages, these will be either before or after the pages // in the viewport. We need to ensure we have one row before and one row after the // focus row to support arrowing up and down. if (focusIndex !== -1) { firstFocusRow = Math.max(0, focusIndex - 3); lastFocusRow = Math.min(rowCount, focusIndex + 3); // Make sure we dont draw any of the pages that are in the viewport. if (firstFocusRow < firstMaterialized) { lastFocusRow = Math.min(lastFocusRow, firstMaterialized - 1); topSpacer1 = firstFocusRow; topSpacer2 = firstMaterialized - lastFocusRow - 1; } else if (lastFocusRow > lastMaterialized) { firstFocusRow = Math.max(firstFocusRow, lastMaterialized + 1); bottomSpacer2 = firstFocusRow - lastMaterialized - 1; bottomSpacer1 = Math.max(0, rowCount - lastFocusRow - 1); } } if (rowProportion < 1) { // Ensure that the spacers leave room for 1 pageSize above the viewport topSpacer2 += Math.min(this.state.pageSize, firstMaterialized); } // console.log({ phase: "render", firstMaterialized, lastMaterialized, topSpacer1, topSpacer2, bottomSpacer2, bottomSpacer1 }); rows.push(this.renderSpacer("st1", topSpacer1)); // If the focus pages are before the viewport render them up to // the first page but not including the first page. if (firstFocusRow < firstMaterialized) { for (var rowIndex = firstFocusRow; rowIndex <= lastFocusRow; rowIndex++) { rows.push(this.renderRow(rowIndex)); } } rows.push(this.renderSpacer("st2", topSpacer2)); // Go through each of the rendered pages and generate the child component. for (var rowIndex = firstMaterialized; rowIndex <= lastMaterialized; rowIndex++) { rows.push(this.renderRow(rowIndex)); } rows.push(this.renderSpacer("sb2", bottomSpacer2, { countFromBottom: true, estimateRowHeight: !this.props.rowHeights })); // If the focus pages are after the last page in the viewport render // them but not including the last page. if (lastFocusRow > lastMaterialized) { for (var rowIndex = firstFocusRow; rowIndex <= lastFocusRow; rowIndex++) { rows.push(this.renderRow(rowIndex)); } } rows.push(this.renderSpacer("sb1", bottomSpacer1, { countFromBottom: true, estimateRowHeight: !this.props.rowHeights })); return (React.createElement(UncheckedObserver, { itemProvider: { // Supply an IObservableExpression to elevate the provider change to a state // update for the entire component instead of just the observer. filter: function (change, action) { // Notify the selection about the change to the items. if (_this.props.selection) { _this.props.selection.onItemsChanged(change, action); } // @NOTE: For now we will just wipe out the entire cache, we can do an optimized // update to the cache based on the rows that changed. var updatedState = { renderedRows: {}, rows: {} }; // If the focused row was removed, we will clear the focus index. if (change.removedItems && _this.focusIndex >= change.index && change.index + change.removedItems.length >= _this.focusIndex) { _this.focusIndex = -1; } // If there is a well defined rowcount we will update it and the maxPage. if (_this.state.rowCount !== -1) { var countChange = (change.addedItems ? change.addedItems.length : 0) - (change.removedItems ? change.removedItems.length : 0); if (countChange) { updatedState.rowCount = _this.state.rowCount + countChange; updatedState.firstMaterialized = Math.max(0, Math.min(_this.state.firstMaterialized, updatedState.rowCount - 1)); updatedState.lastMaterialized = _this.state.virtualize ? Math.max(updatedState.firstMaterialized, Math.min(_this.state.lastMaterialized + (change.index >= _this.state.firstMaterialized && change.index <= _this.state.lastMaterialized + 1 ? Math.min(_this.state.pageSize, countChange) : 0), updatedState.rowCount - 1)) : updatedState.rowCount - 1; } } // console.log(updatedState); _this.setState(updatedState); return false; }, observableValue: this.props.itemProvider } }, React.createElement(FocusWithin, { onBlur: this.onBlur }, function (focusStatus) { // @TODO: Once we get the line-height: 20px in the body the body-m should be removed from the list. var list = (React.createElement("table", { "aria-colcount": useAriaCounts ? (_this.props.ariaColumnCount ? _this.props.ariaColumnCount : _this.props.columnCount) : undefined, "aria-label": _this.props.ariaLabel, "aria-rowcount": useAriaCounts ? _this.state.itemProvider.length + ariaRowOffset : undefined, className: css(className, "bolt-list body-m relative", _this.props.showScroll ? undefined : "scroll-hidden"), id: getSafeId(id), onBlur: focusStatus.onBlur, onClick: _this.onClick, onContextMenu: _this.onDispatch, onDoubleClick: _this.onDoubleClick, onDragEnd: _this.onDispatch, onDragEnter: _this.onDispatch, onDragExit: _this.onDispatch, onDragOver: _this.onDispatch, onDragStart: _this.onDispatch, onDrop: _this.onDispatch, onFocus: focusStatus.onFocus, onKeyDown: _this.onKeyDown, onKeyUp: _this.onDispatch, onPointerDown: _this.onDispatch, ref: _this.listElement, role: role, style: { maxWidth: maxWidth, minWidth: minWidth, width: width }, tabIndex: _this.props.excludeTabStop ? -1 : 0 }, _this.props.renderHeader && _this.props.renderHeader(), React.createElement("tbody", { className: "relative", onFocus: _this.onFocusBody, onPointerDown: _this.onPointerDownBody, ref: _this.bodyElement, role: role === "listbox" || role === "list" || role === "menu" || role === "tree" ? "presentation" : undefined }, _this.renderOverlay(_this.listElement), rows))); if (focuszoneProps) { list = (React.createElement(FocusZone, __assign({}, focuszoneProps, { skipHiddenCheck: true }), list)); } return list; }))); }; List.prototype.componentDidMount = function () { this.context.register(this.onIntersect); if (this.props.virtualize) { document.addEventListener("keydown", this.onVirtualizeKeyDown); } }; List.prototype.componentDidUpdate = function () { var _a = this, scrollToIndex = _a.scrollToIndex, onScrollComplete = _a.onScrollComplete; if (scrollToIndex !== -1 && this.state.rowHeight) { var parentElement = this.bodyElement.current; var _b = this.state, firstMaterialized = _b.firstMaterialized, lastMaterialized = _b.lastMaterialized; // If the row is materialized, we will ensure it is in the viewport. if (scrollToIndex >= firstMaterialized && scrollToIndex <= lastMaterialized && parentElement) { for (var currentIndex = 0; currentIndex < parentElement.children.length; currentIndex++) { var childElement = parentElement.children[currentIndex]; var cellDetails = cellFromElement(childElement); if (cellDetails.rowIndex === scrollToIndex) { childElement.scrollIntoView(this.scrollToOptions); break; } } } // Reset the scroll state before we notify the complete function, it may start a new scroll operation. this.onScrollComplete = undefined; this.scrollToIndex = -1; this.scrollToOptions = undefined; // Notify any pending scrollComplete method that scrolling has completed. if (onScrollComplete) { onScrollComplete(scrollToIndex); } } }; List.prototype.componentWillUnmount = function () { this.context.unregister(this.onIntersect); if (this.props.virtualize) { document.removeEventListener("keydown", this.onVirtualizeKeyDown); } }; List.prototype.addOverlay = function (id, rowIndex, render, zIndex, columnIndex) { if (zIndex === void 0) { zIndex = 0; } var overlays = this.state.overlays; var overlayIndex = overlays.value.findIndex(function (overlay) { return overlay.id === id; }); var rowOverlay = { render: render, id: id, rowIndex: rowIndex, zIndex: zIndex + 1, columnIndex: columnIndex }; // Update the overlay if it exists for that id, otherwise add it if (overlayIndex >= 0) { overlays.change(overlayIndex, rowOverlay); } else { overlays.push(rowOverlay); } }; List.prototype.removeOverlay = function (id) { var overlays = this.state.overlays; var overlayIndex = overlays.value.findIndex(function (overlay) { return overlay.id === id; }); // Remove the overlay if it exists. if (overlayIndex >= 0) { overlays.splice(overlayIndex, 1); } }; List.prototype.getFocusIndex = function () { return this.focusIndex; }; List.prototype.getStats = function () { return { firstMaterialized: this.state.firstMaterialized, firstRendered: this.state.firstRendered, lastMaterialized: this.state.lastMaterialized, lastRendered: this.state.lastRendered }; }; List.prototype.scrollIntoView = function (rowIndex, options, onScrollComplete) { var _a = this.state, firstMaterialized = _a.firstMaterialized, lastMaterialized = _a.lastMaterialized, pageSize = _a.pageSize, rowCount = _a.rowCount, rowHeight = _a.rowHeight, rowProportion = _a.rowProportion; if (rowIndex >= 0 && rowIndex < this.state.rowCount) { var parentElement = this.bodyElement.current; // If the row is materialized, we will ensure it is in the viewport. if (rowIndex >= firstMaterialized && rowIndex <= lastMaterialized && parentElement) { for (var currentIndex = 0; currentIndex < parentElement.children.length; currentIndex++) { var childElement = parentElement.children[currentIndex]; var cellDetails = cellFromElement(childElement); if (cellDetails.rowIndex === rowIndex) { childElement.scrollIntoView(options); break; } } // If the caller wants to know when the scroll has completed, notify them. if (onScrollComplete) { onScrollComplete(rowIndex); } } else { // We only notify the last caller for now, if someone was waiting and another // scroll request was made we will send -1 as the rowIndex scrolled into view. if (this.onScrollComplete) { this.onScrollComplete(-1); } // Set the scrollToOptions that will be applied after the next update. this.onScrollComplete = onScrollComplete; this.scrollToIndex = rowIndex; this.scrollToOptions = options; // We need to add some padding when we grow proportionally, since the spacers do not fill up enough room if // the list starts in the middle of the scrollable region var padding = rowProportion < 1 ? pageSize : 0; // If we havent computed the rowHeight at this point we need to wait until // we know how big rows are to get the row in the right location. if (rowHeight) { this.setState({ firstMaterialized: Math.max(0, rowIndex - padding), lastMaterialized: Math.min(rowCount - 1, rowIndex + padding) }); } } } }; List.prototype.focusRow = function (rowIndex, direction) { var _this = this; if (direction === void 0) { direction = 1; } return new Promise(function (resolve) { _this.scrollIntoView(rowIndex, { block: "center" }, function (completedIndex) { if (completedIndex === rowIndex && _this.bodyElement.current) { var rowElement = _this.bodyElement.current.querySelector("[data-row-index='" + completedIndex + "']"); if (rowElement) { // We need to ensure the requested row is focusable, if not we will move in the // requested direction to find the first focusable row. if (!rowElement.getAttribute("tabindex")) { var newIndex = Math.min(_this.state.rowCount - 1, Math.max(0, completedIndex + direction)); if (newIndex !== completedIndex) { _this.focusRow(newIndex, direction); } else if (newIndex !== _this.focusIndex) { _this.focusRow(newIndex, -direction); } } else { rowElement.focus(); } } } resolve(); }); }); }; List.prototype.processSelectionEvent = function (event, listRow) { var _a = this.props, selection = _a.selection, enforceSingleSelect = _a.enforceSingleSelect; if (!selection || selection.selectable(listRow.index)) { if (selection) { var index = listRow.index; var multiSelect = enforceSingleSelect ? false : selection.multiSelect; // Determine the type of change being made to the selection based on key states. if (this.pivotIndex >= 0 && event.shiftKey && multiSelect) { selection.select(Math.min(this.pivotIndex, index), Math.abs(this.pivotIndex - index) + 1, event.ctrlKey || event.metaKey, multiSelect); } else { var isSpaceBarStroke = event.which === KeyCode.space; if ((event.ctrlKey || event.metaKey || selection.alwaysMerge || isSpaceBarStroke) && multiSelect) { selection.toggle(index, true, multiSelect); } else { selection.select(index, 1, false, multiSelect); } } // Save the last selectionIndex that we selected, this will allow // us to perform range based selection. if (!event.shiftKey) { this.pivotIndex = index; } } this.rowSelected(event, listRow); } }; List.prototype.renderLoadingRow = function (rowIndex, details) { return (React.createElement(ListItem, { className: "bolt-list-row-loading", details: details, index: rowIndex }, React.createElement("div", { className: "shimmer shimmer-line", style: { width: Math.random() * 80 + 20 + "%" } }, "\u00A0"))); }; List.prototype.renderOverlay = function (listElementRef) { var _this = this; var _a = this.state, firstMaterialized = _a.firstMaterialized, lastMaterialized = _a.lastMaterialized, overlays = _a.overlays; return (React.createElement(Observer, { overlays: overlays }, function (props) { var bodyElement = _this.bodyElement.current; if (props.overlays.length > 0 && bodyElement) { return (React.createElement("div", { className: "bolt-list-overlay-container absolute" }, props.overlays.map(function (overlay) { var _a; // Make sure the row is in the rendered range of rows before starting. // Explicitly include column headers at row -1 if (overlay.rowIndex !== -1 && (overlay.rowIndex < firstMaterialized || overlay.rowIndex > lastMaterialized) && !getDragInProgress()) { return null; } // Find the row for the given rowIndex var defaultRowElement = listElementRef.current && listElementRef.current.querySelector("[data-row-index='" + overlay.rowIndex + "']"); var rowElement = _this.props.overlay ? defaultRowElement === null || defaultRowElement === void 0 ? void 0 : defaultRowElement.querySelector(_this.props.overlay) : defaultRowElement; // Special case for column overlay var columnElement = (_a = listElementRef.current) === null || _a === void 0 ? void 0 : _a.querySelector("[data-column-index='" + overlay.columnIndex + "']"); // We cant render the overlay if the row is paged out since we can't determine // the location of the row. if (rowElement) { return !columnElement ? (React.createElement("div", { className: "bolt-list-overlay flex-row absolute", id: getSafeId(overlay.id), key: overlay.id, style: { height: rowElement.offsetHeight, top: rowElement.getBoundingClientRect().top - bodyElement.getBoundingClientRect().top, zIndex: overlay.zIndex * 10 } }, overlay.render({ rowElement: rowElement }))) : (React.createElement("div", { className: "bolt-list-overlay flex-row absolute", id: getSafeId(overlay.id), key: overlay.id, style: { height: rowElement.offsetHeight, width: columnElement.offsetWidth, top: rowElement.getBoundingClientRect().top - bodyElement.getBoundingClientRect().top, left: columnElement.getBoundingClientRect().left - bodyElement.getBoundingClientRect().left, zIndex: overlay.zIndex * 10 } }, overlay.render({ rowElement: columnElement }))); } return null; }))); } return null; })); }; List.prototype.renderRow = function (rowIndex) { var _this = this; var itemProvider = this.props.itemProvider; var _a = this.state, renderedRows = _a.renderedRows, rows = _a.rows; var renderedRow = renderedRows[rowIndex]; if (!renderedRow) { var item_1 = rows[rowIndex]; if (!item_1) { if (itemProvider.getItem) { item_1 = itemProvider.getItem(rowIndex); } else { item_1 = itemProvider.value[rowIndex]; } } // @TODO: If there are no more rows, we need to handle an itemProvider with -1 length. if (!item_1) { return null; } // Save the current item in the item cache. rows[rowIndex] = item_1; var selection = this.props.selection; var selectionObservable = void 0; if (selection) { selectionObservable = { observableValue: selection, filter: function (selectedRanges) { for (var _i = 0, selectedRanges_1 = selectedRanges; _i < selectedRanges_1.length; _i++) { var selectionRange = selectedRanges_1[_i]; if (rowIndex >= selectionRange.beginIndex && rowIndex <= selectionRange.endIndex) { return true; } } return false; } }; } // console.log("render row - " + rowIndex); // Render the row, save it in the cache, and add it to the current page. renderedRow = (React.createElement(UncheckedObserver, { item: item_1, key: rowIndex, selection: selectionObservable }, function (props) { var _a = _this.props, selectableText = _a.selectableText, renderRow = _a.renderRow, renderLoadingRow = _a.renderLoadingRow; var focusIndex = _this.focusIndex; var tabbableIndex = focusIndex >= 0 ? focusIndex : _this.getInitialTabbableRow(); var rowItem = ObservableLike.getValue(item_1); var itemDetails = { selectableText: selectableText, ariaBusy: !props.item, ariaRowOffset: _this.props.ariaRowOffset + 1, data: rowItem, eventDispatch: _this.state.eventDispatch, excludeTabStop: _this.props.excludeTabStop || tabbableIndex !== rowIndex, listProps: _this.props, onFocusItem: _this.onFocusItem, singleClickActivation: _this.props.onActivate && _this.props.singleClickActivation }; if (props.item) { return renderRow(rowIndex, props.item, itemDetails); } else if (renderLoadingRow) { return renderLoadingRow(rowIndex, itemDetails);