azure-devops-ui
Version:
React components for building web UI in Azure DevOps
680 lines (679 loc) • 35.1 kB
JavaScript
import "../../CommonImports";
import "../../Core/core.css";
import "./DropdownList.css";
import "./List.css";
import "./ListDropIndicator.css";
import * as React from "react";
import { ObservableLike, ObservableValue } from '../../Core/Observable';
import { FocusWithin } from '../../FocusWithin';
import { FocusZone, FocusZoneContext, FocusZoneDirection } from '../../FocusZone';
import { IntersectionContext } from '../../Intersection';
import { Observer, UncheckedObserver } from '../../Observer';
import { css, eventTargetContainsNode, getSafeId, KeyCode } from '../../Util';
import { EventDispatch } from '../../Utilities/Dispatch';
/**
* The FixedHeightList component is used to render a collection of items with a series of rows.
*/
export class FixedHeightList extends React.Component {
constructor(props) {
super(props);
// Manage data about pages, including their spacers.
this.intersectionElements = {};
// Track the table element used to render the rows.
this.bodyElement = React.createRef();
this.listElement = React.createRef();
this.scrollToIndex = -1;
this.scrollToOptions = undefined;
// Focus/Selection management members.
this.selectOnFocus = true;
this.focusIndex = new ObservableValue(-1);
this.pivotIndex = -1;
this.onBlur = () => {
this.focusIndex.value = -1;
};
this.onClick = (event) => {
this.onDispatch(event);
if (!event.defaultPrevented) {
if (this.listElement.current) {
const { cellElement, rowIndex } = rowFromEvent(event);
if (!cellElement) {
const item = ObservableLike.getValue(this.state.rows[rowIndex]);
if (rowIndex >= 0 && item) {
const 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 = (event) => {
this.state.eventDispatch.dispatchEvent(event);
};
this.onDoubleClick = (event) => {
this.onDispatch(event);
if (!event.defaultPrevented && !this.props.singleClickActivation) {
const rowIndex = rowFromEvent(event).rowIndex;
const item = ObservableLike.getValue(this.state.rows[rowIndex]);
if (rowIndex >= 0 && item) {
this.rowActivated(event, { data: item, index: rowIndex });
}
}
};
this.onFocusBody = (event) => {
// The first time the list gets focus we need to select initial row if we are performing
// selection of focus.
if (this.selectOnFocus) {
const { selection } = this.props;
if (!selection || selection.selectOnFocus) {
const rowIndex = this.focusIndex.value;
if (rowIndex >= 0) {
const item = ObservableLike.getValue(this.state.rows[rowIndex]);
if (item) {
this.processSelectionEvent(event, { data: item, index: rowIndex });
}
}
}
this.selectOnFocus = false;
}
};
this.onFocusItem = (rowIndex, event) => {
const { focusIndex } = this;
if (focusIndex.value !== rowIndex) {
this.focusRow(rowIndex, 2);
// We need to re-render the previously focused row and newly focused row so we will
// clear the cached values.
if (focusIndex.value >= 0) {
delete this.state.renderedRows[focusIndex.value];
}
else if (this.props.defaultTabbableRow !== undefined) {
// If there was a tabble row that was not the focusIndex.value row we need to update this
// row as well to get it re-rendered without the tabIndex.
delete this.state.renderedRows[this.props.defaultTabbableRow];
}
delete this.state.renderedRows[rowIndex];
this.focusIndex.value = rowIndex;
const item = ObservableLike.getValue(this.state.rows[rowIndex]);
if (item) {
this.rowFocused(event, { data: item, index: rowIndex });
}
}
};
this.onKeyDown = (event) => {
this.onDispatch(event);
if (!event.defaultPrevented) {
const nodeName = event.target.nodeName;
if (nodeName === "INPUT" || nodeName === "TEXTAREA") {
// Don't handle keyboard events when target is an input
return;
}
const { focusIndex } = this;
const item = ObservableLike.getValue(this.state.rows[focusIndex.value]);
if (item) {
if (event.which === KeyCode.enter) {
if (focusIndex.value >= 0 && !eventTargetContainsNode(event, ["A"])) {
this.rowActivated(event, { data: item, index: focusIndex.value });
}
}
else if (event.which === KeyCode.space) {
this.processSelectionEvent(event, { data: item, index: focusIndex.value });
event.preventDefault();
}
else if (event.which === KeyCode.upArrow || event.which === KeyCode.downArrow) {
const { selection } = this.props;
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(() => {
if (this.focusIndex.value != focusIndex.value) {
this.processSelectionEvent(event, { data: item, index: this.focusIndex.value });
}
}, 0);
}
}
else if (event.which === KeyCode.pageDown) {
this.focusRow(Math.min(focusIndex.value + this.props.pageSize, this.state.rowCount - 1), 1);
event.preventDefault();
}
else if (event.which === KeyCode.pageUp) {
this.focusRow(Math.max(focusIndex.value - this.props.pageSize, 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 = (entries) => {
const { scrollTop } = this.context.root;
const { rowCount } = this.state;
const { firstMaterialized, lastMaterialized } = this.state;
const { rowHeight, rowProportion } = this.state;
// Don't process an intersection while scroll event is pending.
if (scrollTop !== this.state.scrollTop && entries.length) {
return;
}
// Ignore events if we dont have a our basic elements resolved (this should never happen).
if (!this.listElement.current || !this.bodyElement.current) {
return;
}
// Determine the location of the intersection within the page. This is the element
// we are scrolling within.
const intersectionRect = this.context.root.getBoundingClientRect();
const scrollTopRect = Math.max(0, scrollTop + this.context.root.offsetTop - this.listElement.current.offsetTop);
// Track the first and last row elements for adjusting the range.
let firstMaterializedUpdated = Math.max(0, Math.min(rowCount - 1, Math.floor(scrollTopRect / (rowHeight * rowProportion))));
let lastMaterializedUpdated = Math.min(rowCount - 1, firstMaterializedUpdated + Math.ceil(intersectionRect.height / rowHeight));
if (scrollTopRect + (lastMaterializedUpdated - firstMaterializedUpdated) * rowHeight > this.state.maxHeight) {
lastMaterializedUpdated = rowCount - 1;
firstMaterializedUpdated = Math.max(0, lastMaterializedUpdated - Math.ceil(intersectionRect.height / rowHeight));
}
// Update our state if and only if something has changed.
if (firstMaterializedUpdated !== firstMaterialized ||
lastMaterializedUpdated !== lastMaterialized ||
rowHeight !== this.state.rowHeight ||
scrollTop !== this.state.scrollTop ||
scrollTopRect !== this.state.scrollTopRect) {
//
// @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.
//
this.setState({
firstMaterialized: firstMaterializedUpdated,
lastMaterialized: lastMaterializedUpdated,
rowHeight,
scrollTop,
scrollTopRect
});
}
};
this.onMouseDownBody = (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;
};
const rowCount = props.itemProvider.length;
this.state = {
eventDispatch: props.eventDispatch || new EventDispatch(),
firstMaterialized: 0,
itemProvider: props.itemProvider,
lastMaterialized: 0,
maxHeight: this.props.maxHeight || 1000000,
focusRows: {},
renderedRows: {},
rowCount,
rowHeight: props.rowHeight || 0,
rowProportion: props.rowHeight && props.maxHeight ? Math.min(1, props.maxHeight / (props.rowHeight * rowCount)) : 1,
rows: {},
scrollTop: 0,
scrollTopRect: 0
};
}
static getDerivedStateFromProps(props, state) {
const rowCount = props.itemProvider.length;
let firstMaterialized = state.firstMaterialized;
let lastMaterialized = state.lastMaterialized;
if (rowCount !== state.rowCount) {
firstMaterialized = Math.max(0, Math.min(state.firstMaterialized, rowCount));
lastMaterialized = Math.max(firstMaterialized, Math.min(state.lastMaterialized + (state.lastMaterialized === state.rowCount - 1 ? props.pageSize : 0), rowCount - 1));
}
// Ensure out pages and providers are appropriately computed.
const updatedState = {
firstMaterialized,
itemProvider: props.itemProvider,
lastMaterialized,
rowCount,
rowProportion: Math.min(1, state.maxHeight / (state.rowHeight * rowCount))
};
// If there are changes to the props that affect the cached data, we need it clear it.
if (props.itemProvider !== state.itemProvider) {
updatedState.renderedRows = {};
updatedState.rows = {};
}
return updatedState;
}
getListRole() {
return this.props.role ? this.props.role : this.props.selection ? "listbox" : "list";
}
getItemRole() {
switch (this.getListRole()) {
case "tree":
case "group":
return "treeitem";
case "list":
return "listitem";
case "listbox":
return "option";
case "radiogroup":
return "radio";
default:
return null;
}
}
render() {
const { className, focuszoneProps, id, width } = this.props;
const { firstMaterialized, lastMaterialized, maxHeight, rowCount, rowHeight } = this.state;
const role = this.getListRole();
const rows = [];
const firstFocusRow = Math.max(0, this.focusIndex.value - 3);
const lastFocusRow = Math.min(rowCount, this.focusIndex.value + 3);
rows.push(this.renderIntersectionBounds(true));
// Add focus rows around rendered rows.
if (this.focusIndex.value !== -1 && firstFocusRow < firstMaterialized) {
for (let rowIndex = firstFocusRow; rowIndex <= Math.min(lastFocusRow, firstMaterialized - 1); rowIndex++) {
rows.push(this.renderRow(rowIndex, false));
}
}
for (let rowIndex = firstMaterialized; rowIndex <= lastMaterialized; rowIndex++) {
rows.push(this.renderRow(rowIndex, true));
}
if (this.focusIndex.value !== -1 && lastFocusRow > lastMaterialized && lastMaterialized > 0) {
for (let rowIndex = Math.max(firstFocusRow, lastMaterialized + 1); rowIndex <= lastFocusRow; rowIndex++) {
rows.push(this.renderRow(rowIndex, false));
}
}
rows.push(this.renderIntersectionBounds(false));
const height = Math.min(maxHeight, rowHeight * this.state.rowCount);
let list = (React.createElement("div", { "aria-label": this.props.ariaLabel, className: css(className, "bolt-fixed-height-list relative"), id: getSafeId(id), onBlur: this.onBlur, onClick: this.onClick, onDoubleClick: this.onDoubleClick, onDragEnd: this.onDispatch, onDragEnter: this.onDispatch, onDragExit: this.onDispatch, onDragOver: this.onDispatch, onDragStart: this.onDispatch, onDrop: this.onDispatch, onKeyUp: this.onDispatch, onMouseDown: this.onDispatch, onTouchStart: this.onDispatch, ref: this.listElement, role: role, style: { width, height: height } },
React.createElement("div", { className: "relative", onFocus: this.onFocusBody, onKeyDown: this.onKeyDown, onMouseDown: this.onMouseDownBody, ref: this.bodyElement, style: { width, height: height } }, rows)));
list = (React.createElement(FocusZone, Object.assign({ direction: FocusZoneDirection.Vertical, skipHiddenCheck: true }, focuszoneProps), list));
return (React.createElement(Observer, { itemProvider: {
// Supply an IObservableExpression to elevate the provider change to a state
// update for the entire component instead of just the observer.
filter: (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.
const updatedState = {
renderedRows: {},
focusRows: {},
rows: {}
};
// If their is a well defined rowcount we will update it and the maxPage.
if (this.state.rowCount !== -1) {
const 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 = Math.max(updatedState.firstMaterialized, Math.min(this.state.lastMaterialized +
(change.index >= this.state.firstMaterialized && change.index <= this.state.lastMaterialized + 1
? countChange
: 0), updatedState.rowCount - 1));
}
}
this.setState(updatedState);
return false;
},
observableValue: this.props.itemProvider
} }, () => list));
}
componentDidMount() {
this.onIntersect([]);
this.context.register(this.onIntersect);
}
componentDidUpdate(prevProps, prevState) {
const { scrollToIndex, onScrollComplete } = this;
if (this.state.rowCount !== prevState.rowCount) {
this.onIntersect([]);
}
if (scrollToIndex !== -1 && this.state.rowHeight) {
const parentElement = this.bodyElement.current;
const { firstMaterialized, lastMaterialized } = this.state;
// If the row is materialized, we will ensure it is in the viewport.
if (scrollToIndex >= firstMaterialized && scrollToIndex <= lastMaterialized && parentElement) {
for (let currentIndex = 0; currentIndex < parentElement.children.length; currentIndex++) {
const childElement = parentElement.children[currentIndex];
const cellDetails = rowFromElement(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);
}
}
}
componentWillUnmount() {
this.context.unregister(this.onIntersect);
}
getFocusIndex() {
return this.focusIndex.value;
}
getStats() {
return {
firstMaterialized: this.state.firstMaterialized,
lastMaterialized: this.state.lastMaterialized
};
}
scrollIntoView(rowIndex, options, onScrollComplete) {
const { pageSize } = this.props;
const { firstMaterialized, lastMaterialized, rowCount } = this.state;
if (rowIndex >= 0 && rowIndex < this.state.rowCount) {
const parentElement = this.bodyElement.current;
// If the row is materialized, we will ensure it is in the viewport.
if (rowIndex >= firstMaterialized && rowIndex <= lastMaterialized && parentElement) {
for (let currentIndex = 0; currentIndex < parentElement.children.length; currentIndex++) {
const childElement = parentElement.children[currentIndex];
const cellDetails = rowFromElement(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;
// 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.
this.setState({
firstMaterialized: Math.max(0, rowIndex - Math.floor((lastMaterialized - firstMaterialized) / 2)),
lastMaterialized: Math.min(rowCount - 1, Math.ceil(rowIndex + (lastMaterialized - firstMaterialized) / 2))
});
}
}
}
focusRow(rowIndex, direction) {
this.scrollIntoView(rowIndex, { block: "nearest" }, (completedIndex) => {
if (completedIndex === rowIndex && this.bodyElement.current) {
const 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")) {
const newIndex = Math.min(this.state.rowCount - 1, Math.max(0, completedIndex + direction));
if (newIndex !== completedIndex) {
this.focusRow(newIndex, direction);
}
else if (newIndex !== this.focusIndex.value) {
this.focusRow(newIndex, -direction);
}
}
else {
// Set focus to the row that scroll to
rowElement.focus();
}
}
}
});
}
processSelectionEvent(event, listRow) {
const { selection } = this.props;
if (!selection || selection.selectable(listRow.index)) {
let initialState = false;
let targetState = true;
if (selection) {
const { index } = listRow;
// If a selection is available use it to track the initial state.
initialState = selection.selected(index);
// Determine the type of change being made to the selection based on key states.
if (this.pivotIndex >= 0 && event.shiftKey && selection.multiSelect) {
selection.select(Math.min(this.pivotIndex, index), Math.abs(this.pivotIndex - index) + 1, event.ctrlKey || event.metaKey);
}
else {
if ((event.ctrlKey || event.metaKey || selection.alwaysMerge) && selection.multiSelect) {
selection.toggle(index, true);
targetState = false;
}
else {
selection.select(index, 1, false);
}
}
// Save the last selectionIndex that we selected, this will allow
// us to perform range based selection.
if (!event.shiftKey) {
this.pivotIndex = index;
}
}
if (initialState !== targetState) {
this.rowSelected(event, listRow);
}
}
}
renderLoadingRow(rowIndex, details) {
return (React.createElement("div", { className: "bolt-list-row-loading" },
React.createElement("div", { className: "shimmer shimmer-line", style: { width: Math.random() * 80 + 20 + "%" } }, "\u00A0")));
}
renderIntersectionBounds(top) {
const { firstMaterialized, lastMaterialized, rowHeight, rowProportion } = this.state;
const key = top ? "topobserv" : "bottomobserv";
let rowTop = 0;
// If we run out of room move from the bottom up. This can happen with proportionally allocated rows
if (firstMaterialized * rowHeight * rowProportion + (lastMaterialized - firstMaterialized) * rowHeight > this.state.maxHeight) {
if (top) {
rowTop = this.state.maxHeight;
rowTop -= (lastMaterialized - firstMaterialized) * rowHeight * rowProportion + rowHeight;
rowTop--;
}
else {
rowTop = this.state.maxHeight - 1;
}
}
else {
if (top) {
rowTop = firstMaterialized * rowHeight * rowProportion - 1;
}
else {
rowTop = firstMaterialized * rowHeight * rowProportion + (1 + lastMaterialized - firstMaterialized) * rowHeight + 1;
}
}
return (React.createElement("div", { className: "bolt-list-row-spacer invisible absolute", key: key, ref: spacerElement => {
const existingElement = this.intersectionElements[key];
if (spacerElement) {
if (existingElement !== spacerElement) {
if (existingElement) {
this.context.unobserve(spacerElement);
}
this.context.observe(spacerElement);
this.intersectionElements[key] = spacerElement;
}
}
else if (existingElement) {
this.context.unobserve(existingElement);
delete this.intersectionElements[key];
}
}, role: "presentation", style: { top: `${rowTop}px`, height: "1px" } }));
}
renderRow(rowIndex, isVisible) {
const { itemProvider } = this.props;
const { focusRows, renderedRows, firstMaterialized, lastMaterialized, rowHeight, rowProportion, rows } = this.state;
const role = this.getItemRole();
let renderedRow = isVisible ? renderedRows[rowIndex] : focusRows[rowIndex];
// We can't use the cache for proportioned rows since the top is different based on what the firstMaterialized value is
if (!renderedRow || rowProportion !== 1) {
let item = rows[rowIndex];
if (!item) {
if (itemProvider.getItem) {
item = itemProvider.getItem(rowIndex);
}
else {
item = itemProvider.value[rowIndex];
}
}
// @TODO: If there are no more rows, we need to handle an itemProvider with -1 length.
if (!item) {
return null;
}
// Save the current item in the item cache.
rows[rowIndex] = item;
const { selection } = this.props;
let selectionObservable;
if (selection) {
selectionObservable = {
observableValue: selection,
filter: (selectedRanges) => {
for (const selectionRange of selectedRanges) {
if (rowIndex >= selectionRange.beginIndex && rowIndex <= selectionRange.endIndex) {
return true;
}
}
return false;
}
};
}
const onFocus = (event) => {
this.onFocusItem(rowIndex, event);
};
// Render the row, save it in the cache, and add it to the current page.
renderedRow = (React.createElement(UncheckedObserver, { item: item, key: rowIndex, selection: selectionObservable, focusIndex: this.focusIndex }, (props) => {
var _a, _b;
const { renderRow, renderLoadingRow } = this.props;
const { rowHeight, rowCount } = this.state;
const rowItem = ObservableLike.getValue(item);
const itemDetails = {
ariaBusy: !props.item,
ariaRowOffset: 1,
data: rowItem,
eventDispatch: this.state.eventDispatch,
itemProvider: this.props.itemProvider,
listProps: this.props,
onFocusItem: this.onFocusItem,
singleClickActivation: this.props.onActivate && this.props.singleClickActivation
};
let renderedRow;
if (props.item) {
renderedRow = renderRow(rowIndex, props.item, itemDetails);
}
else if (renderLoadingRow) {
renderedRow = renderLoadingRow(rowIndex, itemDetails);
}
else {
renderedRow = this.renderLoadingRow(rowIndex, itemDetails);
}
let rowTop = 0;
let rowHeightSpace = 0;
if (rowIndex >= firstMaterialized && rowIndex <= lastMaterialized) {
rowHeightSpace = rowHeight;
}
// If we run out of room move from the bottom up. This can happen with proportionally allocated rows
if (firstMaterialized * rowHeight * rowProportion + (lastMaterialized - firstMaterialized) * rowHeight >
this.state.maxHeight) {
rowTop = this.state.maxHeight;
rowTop -= (rowCount - lastMaterialized) * rowHeight * rowProportion;
rowTop -= (lastMaterialized - rowIndex) * rowHeight;
}
else {
if (rowHeightSpace === 0) {
rowTop = rowIndex * rowHeight * rowProportion;
}
else {
rowTop = firstMaterialized * rowHeight * rowProportion;
rowTop += (rowIndex - firstMaterialized) * rowHeight;
}
}
const rowData = itemDetails === null || itemDetails === void 0 ? void 0 : itemDetails.data;
const hasChildItems = (_a = rowData === null || rowData === void 0 ? void 0 : rowData.underlyingItem) === null || _a === void 0 ? void 0 : _a.childItems;
const isExpanded = (_b = rowData === null || rowData === void 0 ? void 0 : rowData.underlyingItem) === null || _b === void 0 ? void 0 : _b.expanded;
return (React.createElement(FocusWithin, { onFocus: onFocus }, (focusStatus) => {
return (React.createElement(FocusZoneContext.Consumer, null, rowContext => {
return (React.createElement(FocusZone, { direction: FocusZoneDirection.Horizontal },
React.createElement("div", { className: css("bolt-fixed-height-list-row scroll-hidden absolute", this.focusIndex.value === rowIndex && "focused"), style: {
height: `${rowHeightSpace}px`,
top: `${rowTop}px`
}, "data-focuszone": rowContext.focuszoneId, "data-row-index": rowIndex, tabIndex: rowIndex == 0 || hasChildItems ? 0 : -1, onBlur: focusStatus.onBlur, onFocus: focusStatus.onFocus, role: role, "aria-expanded": hasChildItems && isExpanded === undefined ? false : isExpanded, "aria-labelledby": `rowContent-${rowIndex}` }, renderedRow)));
}));
}));
}));
// Save the row in our cache.
if (isVisible) {
this.state.renderedRows[rowIndex] = renderedRow;
}
else {
this.state.focusRows[rowIndex] = renderedRow;
}
}
return renderedRow;
}
rowActivated(event, listRow) {
this.state.eventDispatch.dispatchEvent(event, listRow, "activate");
if (this.props.onActivate) {
this.props.onActivate(event, listRow);
}
}
rowSelected(event, listRow) {
this.state.eventDispatch.dispatchEvent(event, listRow, "select");
if (this.props.onSelect) {
this.props.onSelect(event, listRow);
}
}
rowFocused(event, listRow) {
this.state.eventDispatch.dispatchEvent(event, listRow, "focus");
if (this.props.onFocus) {
this.props.onFocus(event, listRow);
}
}
}
FixedHeightList.contextType = IntersectionContext;
FixedHeightList.defaultProps = {
defaultTabbableRow: 0,
focuszoneProps: { direction: FocusZoneDirection.Vertical },
maxHeight: 1000000
};
function getAttributeAsNumber(element, attributeName) {
const attributeValue = element.getAttribute(attributeName);
if (attributeValue) {
return parseInt(attributeValue, 10);
}
return -1;
}
export function rowFromElement(element) {
let attributeValue;
const cellIndex = -1;
let rowIndex = -1;
const cellElement = null;
while (element) {
attributeValue = getAttributeAsNumber(element, "data-row-index");
if (attributeValue !== -1) {
rowIndex = attributeValue;
break;
}
// We have hit the root of the details list, dont look above this.
if (element.classList.contains("bolt-fixed-height-list")) {
element = null;
break;
}
element = element.parentElement;
}
return {
cellElement,
cellIndex,
rowElement: element,
rowIndex
};
}
export function rowFromEvent(event) {
return rowFromElement(event.target);
}