terra-data-grid
Version:
Package containing data grid container components that enable users to navigate the grid information using directional navigation keys.
509 lines (506 loc) • 23.1 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "ColumnHighlightColor", {
enumerable: true,
get: function get() {
return _DataGrid.ColumnHighlightColor;
}
});
exports.default = void 0;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _reactIntl = require("react-intl");
var _bind = _interopRequireDefault(require("classnames/bind"));
var KeyCode = _interopRequireWildcard(require("keycode-js"));
var _terraVisuallyHiddenText = _interopRequireDefault(require("terra-visually-hidden-text"));
var _terraTable = require("terra-table");
var _DataGrid = _interopRequireWildcard(require("./DataGrid"));
var _FlowsheetDataGridModule = _interopRequireDefault(require("./FlowsheetDataGrid.module.scss"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
var cx = _bind.default.bind(_FlowsheetDataGridModule.default);
var propTypes = {
/**
* String that identifies the element (or elements) that labels the grid.
*/
ariaLabelledBy: _propTypes.default.string,
/**
* String that labels the grid for accessibility. If ariaLabelledBy is specified, ariaLabel will not be used.
*/
ariaLabel: _propTypes.default.string,
/**
* String that will be used to identify the Grid. If multiple grids are on the same page, each grid should have
* a unique id.
*/
id: _propTypes.default.string.isRequired,
/**
* Data for content in the body of the Grid. Rows will be rendered in the order given. The first cell in each row will be the row header.
*/
rows: _propTypes.default.arrayOf(_terraTable.rowShape),
/**
* Data for content in the body of the table. Sections will be rendered in the order given.
*/
sections: _propTypes.default.arrayOf(_terraTable.sectionShape),
/**
* Data for columns. Columns will be presented in the order given.
* The first column, pinned to the leftmost side of the grid, will contain row headers.
* The remaining columns will be rendered in the horizontal overflow.
*/
columns: _propTypes.default.arrayOf(_terraTable.columnShape),
/**
* Number indicating the default column width in px. This value will be used if no overriding width value is provided on a per-column basis.
*/
defaultColumnWidth: _propTypes.default.number,
/**
* String that specifies the column height. Any valid CSS height value is accepted.
*/
columnHeaderHeight: _propTypes.default.string,
/**
* String that specifies the height for the rows in the grid. Any valid CSS value is accepted.
*/
rowHeight: _propTypes.default.string,
/**
* String that specifies the minimum height for the rows on the table. rowHeight takes precedence if valid CSS value is passed.
* With this property the height of the cell will grow to fit the cell content.
*/
rowMinimumHeight: _propTypes.default.string,
/**
* Callback function that is called when a selectable cell is selected. Parameters:
* @param {object} selectedCell object containing rowId, columnId, columnSpanIndex and sectionId.
* @param {object} event JavaScript event object.
*/
onCellSelect: _propTypes.default.func,
/**
* Function that is called when a collapsible section is selected. Parameters:
* @param {string} sectionId sectionId
*/
onSectionSelect: _propTypes.default.func,
/**
* Callback function that is called when all selected cells need to be unselected. Parameters: none.
*/
onClearSelectedCells: _propTypes.default.func,
/**
* Callback function that is called when a range of selectable cells is selected. Parameters:
* @param {array} cells - Array of cells each containing a rowId, columnId, and sectionId, all as strings.
*/
onCellRangeSelect: _propTypes.default.func,
/**
* Callback function that is called when a row header cell is selected. Parameters:
* @param {object} rowToSelect object containing rowId and sectionId, both as strings.
* @param {boolean} isMetaPressed isMetaPressed
*/
onRowSelect: _propTypes.default.func,
/**
* @private
* The intl object containing translations. This is retrieved from the context automatically by injectIntl.
*/
intl: _propTypes.default.shape({
formatMessage: _propTypes.default.func
}).isRequired,
/**
* Boolean to show/hide column headers. By default, it is set to `true` and column headers are visible.
*/
hasVisibleColumnHeaders: _propTypes.default.bool,
/**
* Determines if focus is moved to the interactive element of a cell when a single button or hyperlink element is the only interactive element.
*/
isAutoFocusEnabled: _propTypes.default.bool
};
var defaultProps = {
defaultColumnWidth: 200,
columnHeaderHeight: '2.5rem',
rows: [],
columns: [],
hasVisibleColumnHeaders: true
};
function FlowsheetDataGrid(props) {
var id = props.id,
ariaLabelledBy = props.ariaLabelledBy,
ariaLabel = props.ariaLabel,
rows = props.rows,
sections = props.sections,
columns = props.columns,
defaultColumnWidth = props.defaultColumnWidth,
columnHeaderHeight = props.columnHeaderHeight,
rowHeight = props.rowHeight,
onCellSelect = props.onCellSelect,
onSectionSelect = props.onSectionSelect,
onClearSelectedCells = props.onClearSelectedCells,
onCellRangeSelect = props.onCellRangeSelect,
onRowSelect = props.onRowSelect,
intl = props.intl,
hasVisibleColumnHeaders = props.hasVisibleColumnHeaders,
rowMinimumHeight = props.rowMinimumHeight,
isAutoFocusEnabled = props.isAutoFocusEnabled;
var anchorCell = (0, _react.useRef)(null);
var selectedCells = (0, _react.useRef)([]);
var _useState = (0, _react.useState)(null),
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
cellSelectionAriaLiveMessage = _useState2[0],
setCellSelectionAriaLiveMessage = _useState2[1];
var inShiftDirectionalMode = (0, _react.useRef)(false);
var dataGridFuncRef = (0, _react.useRef)();
var flowsheetColumns = (0, _react.useMemo)(function () {
return columns.map(function (column) {
return _objectSpread(_objectSpread({}, column), {}, {
isSelectable: false,
isResizable: false
});
});
}, [columns]);
var pinnedColumns = flowsheetColumns.length ? [flowsheetColumns[0]] : [];
var overflowColumns = flowsheetColumns.length > 1 ? flowsheetColumns.slice(1) : [];
var contentHasNoResult = function contentHasNoResult(content) {
return content === null || content === '' || content === '--';
};
var flowsheetRows = (0, _react.useMemo)(function () {
var noResultCellContent = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("span", {
"aria-hidden": true
}, intl.formatMessage({
id: 'Terra.flowsheetDataGrid.no-result-display'
})), /*#__PURE__*/_react.default.createElement(_terraVisuallyHiddenText.default, {
text: intl.formatMessage({
id: 'Terra.flowsheetDataGrid.no-result'
})
}));
var newRows = (0, _toConsumableArray2.default)(rows);
newRows.forEach(function (row, rowIndex) {
var newCells = (0, _toConsumableArray2.default)(row.cells);
newCells.forEach(function (cell, cellIndex) {
// All flowsheet cells are selectable.
newCells[cellIndex].isSelectable = true;
// Cell content has no result and is not a row header (first column), set content to "No result".
if (contentHasNoResult(cell.content) && cellIndex !== 0) {
newCells[cellIndex].content = noResultCellContent;
}
});
newRows[rowIndex].cells = newCells;
});
return newRows;
}, [intl, rows]);
var flowsheetSections = (0, _react.useMemo)(function () {
if (!sections) {
return null;
}
var noResultCellContent = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("span", {
"aria-hidden": true
}, intl.formatMessage({
id: 'Terra.flowsheetDataGrid.no-result-display'
})), /*#__PURE__*/_react.default.createElement(_terraVisuallyHiddenText.default, {
text: intl.formatMessage({
id: 'Terra.flowsheetDataGrid.no-result'
})
}));
var newSections = (0, _toConsumableArray2.default)(sections);
newSections.forEach(function (section, sectionIndex) {
if (section.rows) {
var newRows = (0, _toConsumableArray2.default)(section.rows);
newRows.forEach(function (row, rowIndex) {
var newCells = (0, _toConsumableArray2.default)(row.cells);
newCells.forEach(function (cell, cellIndex) {
newCells[cellIndex].isSelectable = cell.isSelectable !== false;
// Cell content has no result and is not a row header (first column), set content to "No result".
if (contentHasNoResult(cell.content) && cellIndex !== 0) {
newCells[cellIndex].content = noResultCellContent;
}
});
newRows[rowIndex].cells = newCells;
});
newSections[sectionIndex].rows = newRows;
}
if (section.subsections) {
var newSubsections = (0, _toConsumableArray2.default)(section.subsections);
newSubsections.forEach(function (subsection, subsectionIndex) {
var newSubsectionRows = (0, _toConsumableArray2.default)(subsection.rows);
newSubsectionRows.forEach(function (subsectionRow, subsectionRowIndex) {
var newSubsectionCells = (0, _toConsumableArray2.default)(subsectionRow.cells);
newSubsectionCells.forEach(function (subsectionCell, subsectionCellIndex) {
newSubsectionCells[subsectionCellIndex].isSelectable = subsectionCell.isSelectable !== false;
// Cell content has no result and is not a row header (first column), set content to "No result".
if (contentHasNoResult(subsectionCell.content) && subsectionCellIndex !== 0) {
newSubsectionCells[subsectionCellIndex].content = noResultCellContent;
}
});
newSubsectionRows[subsectionRowIndex].cells = newSubsectionCells;
});
newSubsections[subsectionIndex].rows = newSubsectionRows;
});
}
});
return newSections;
}, [intl, sections]);
var rowsToSearch = (0, _react.useMemo)(function () {
if (flowsheetSections) {
return flowsheetSections.flatMap(function (section) {
if (section.rows) {
return section.rows.map(function (row) {
return _objectSpread(_objectSpread({}, row), {}, {
sectionId: section.id
});
});
}
return section.subsections.flatMap(function (subsection) {
return subsection.rows.map(function (row) {
return _objectSpread(_objectSpread({}, row), {}, {
sectionId: section.id,
subsectionId: subsection.id
});
});
});
});
}
return flowsheetRows;
}, [flowsheetSections, flowsheetRows]);
(0, _react.useEffect)(function () {
var previousSelectedCells = (0, _toConsumableArray2.default)(selectedCells.current);
var newSelectedCells = [];
var tableBodyColumnsRef = dataGridFuncRef.current.getTableBodyColumnsRef();
rowsToSearch.forEach(function (row) {
row.cells.forEach(function (cell, cellIndex) {
if (cell.isSelected) {
newSelectedCells.push({
rowId: row.id,
columnId: tableBodyColumnsRef[cellIndex].id,
columnSpanIndex: tableBodyColumnsRef[cellIndex].columnSpanIndex
});
}
});
});
selectedCells.current = [].concat(newSelectedCells);
if (!selectedCells.current.length) {
anchorCell.current = null;
}
if (previousSelectedCells.length > 0 && selectedCells.current.length === 0) {
setCellSelectionAriaLiveMessage(intl.formatMessage({
id: 'Terra.flowsheetDataGrid.no-cells-selected'
}));
} else if (selectedCells.current.length) {
setCellSelectionAriaLiveMessage(intl.formatMessage({
id: 'Terra.flowsheetDataGrid.cells-selected'
}, {
cellCount: selectedCells.current.length
}));
}
}, [intl, rowsToSearch, setCellSelectionAriaLiveMessage]);
var selectCellRange = (0, _react.useCallback)(function (rowId, columnId, columnIndex, sectionId, subsectionId) {
if (anchorCell.current === null) {
return;
}
var anchorRowIndex = rowsToSearch.findIndex(function (row) {
return row.id === anchorCell.current.rowId;
});
var anchorColumnIndex = anchorCell.current.columnIndex;
var rowIndex = rowsToSearch.findIndex(function (row) {
return row.id === rowId;
});
var tableBodyColumnsRef = dataGridFuncRef.current.getTableBodyColumnsRef();
// Determine the boundaries of selected region.
var rowIndexTopBound = Math.min(anchorRowIndex, rowIndex);
var rowIndexBottomBound = Math.max(anchorRowIndex, rowIndex);
var columnIndexLeftBound = Math.min(anchorColumnIndex, columnIndex);
var columnIndexRightBound = Math.max(anchorColumnIndex, columnIndex);
if (flowsheetSections) {
var selectedSection = flowsheetSections.find(function (section) {
return section.id === sectionId;
});
var sectionTopBound = 0;
var sectionBottomBound = 0;
if (subsectionId) {
var selectedSubsection = selectedSection.subsections.find(function (subsection) {
return subsection.id === subsectionId;
});
sectionTopBound = rowsToSearch.findIndex(function (row) {
return row.subsectionId === subsectionId;
});
sectionBottomBound = sectionTopBound + selectedSubsection.rows.length - 1;
} else {
sectionTopBound = rowsToSearch.findIndex(function (row) {
return row.sectionId === sectionId;
});
sectionBottomBound = sectionTopBound + selectedSection.rows.length - 1;
}
rowIndexTopBound = anchorRowIndex <= sectionTopBound ? sectionTopBound : rowIndexTopBound;
rowIndexBottomBound = anchorRowIndex >= sectionBottomBound ? sectionBottomBound : rowIndexBottomBound;
}
var cellsToSelect = [];
for (var rowIdx = rowIndexTopBound; rowIdx <= rowIndexBottomBound; rowIdx += 1) {
var rowIdToSelect = rowsToSearch[rowIdx].id;
for (var colIdx = columnIndexLeftBound; colIdx <= columnIndexRightBound; colIdx += 1) {
var columnSpanIndex = tableBodyColumnsRef[colIdx].columnSpanIndex;
var columnIdToSelect = tableBodyColumnsRef[colIdx].id;
cellsToSelect.push({
rowId: rowIdToSelect,
columnId: columnIdToSelect,
columnIndex: colIdx,
sectionId: sectionId,
columnSpanIndex: columnSpanIndex
});
}
}
if (onCellRangeSelect) {
onCellRangeSelect(cellsToSelect);
}
}, [rowsToSearch, flowsheetSections, onCellRangeSelect]);
var handleCellSelection = (0, _react.useCallback)(function (selectionDetails, event) {
// Call onRowSelect for row header column
if (selectionDetails.columnIndex === 0) {
if (onRowSelect) {
onRowSelect({
rowId: selectionDetails.rowId,
sectionId: selectionDetails.sectionId,
isMetaPressed: selectionDetails.isMetaPressed
});
}
} else if (selectionDetails.isShiftPressed && anchorCell.current !== null) {
selectCellRange(selectionDetails.rowId, selectionDetails.columnId, selectionDetails.columnIndex, selectionDetails.sectionId, selectionDetails.subsectionId, selectionDetails.columnSpanIndex);
} else if (onCellSelect) {
anchorCell.current = {
rowId: selectionDetails.rowId,
columnId: selectionDetails.columnId,
columnIndex: selectionDetails.columnIndex,
sectionId: selectionDetails.sectionId
};
onCellSelect({
rowId: selectionDetails.rowId,
columnId: selectionDetails.columnId,
columnIndex: selectionDetails.columnIndex,
columnSpanIndex: selectionDetails.columnSpanIndex,
sectionId: selectionDetails.sectionId,
subsectionId: selectionDetails.subsectionId,
isMetaPressed: selectionDetails.isMetaPressed
}, event);
}
}, [onCellSelect, onRowSelect, selectCellRange]);
var handleCellRangeSelection = (0, _react.useCallback)(function (rowIndex, columnIndex, direction) {
// Exclude the row header column as an eligible anchor/start cell.
if (columnIndex <= 0 && !inShiftDirectionalMode.current) {
return;
}
var gridRef = dataGridFuncRef.current.getGridRef();
var tableBodyColumnsRef = dataGridFuncRef.current.getTableBodyColumnsRef();
var anchorSectionId = '';
if (flowsheetSections) {
for (var rowIdx = rowIndex; rowIdx > 0; rowIdx -= 1) {
var currentSectionId = gridRef.rows[rowIdx].getAttribute('data-section-id');
if (currentSectionId) {
anchorSectionId = currentSectionId;
break;
}
}
}
if (!inShiftDirectionalMode.current) {
// Start of range selection using Shift+Up/Down/Left/Right so save this as the anchor/start for the range if one does not exist.
inShiftDirectionalMode.current = true;
if (anchorCell.current === null && !gridRef.rows[rowIndex].hasAttribute('data-section-id')) {
anchorCell.current = {
rowId: gridRef.rows[rowIndex].getAttribute('data-row-id'),
columnId: tableBodyColumnsRef[columnIndex].id,
columnIndex: columnIndex,
sectionId: anchorSectionId
};
}
}
var nextRowIndex = rowIndex;
var nextColumnIndex = columnIndex;
switch (direction) {
case KeyCode.KEY_UP:
nextRowIndex -= 1;
break;
case KeyCode.KEY_DOWN:
nextRowIndex += 1;
break;
case KeyCode.KEY_LEFT:
nextColumnIndex -= 1;
break;
case KeyCode.KEY_RIGHT:
nextColumnIndex += 1;
break;
default:
break;
}
// Reset to valid selectable index if outer bounds reached.
if (nextRowIndex <= 0) {
nextRowIndex = 1;
} else if (nextRowIndex >= gridRef.rows.length) {
nextRowIndex = gridRef.rows.length - 1;
}
if (nextColumnIndex <= 0) {
nextColumnIndex = 1;
} else if (nextColumnIndex >= tableBodyColumnsRef.length) {
nextColumnIndex = tableBodyColumnsRef.length - 1;
}
var nextColumnId = tableBodyColumnsRef[nextColumnIndex].id;
var nextRowId = gridRef.rows[nextRowIndex].getAttribute('data-row-id');
if (!gridRef.rows[nextRowIndex].hasAttribute('data-section-id') && !gridRef.rows[nextRowIndex].hasAttribute('data-subsection-id')) {
var nextSectionId = '';
var nextSubsectionId = '';
for (var _rowIdx = nextRowIndex; _rowIdx > 0; _rowIdx -= 1) {
if (gridRef.rows[_rowIdx].hasAttribute('data-section-id')) {
nextSectionId = gridRef.rows[_rowIdx].getAttribute('data-section-id');
break;
}
if (gridRef.rows[_rowIdx].hasAttribute('data-subsection-id')) {
nextSubsectionId = gridRef.rows[_rowIdx].getAttribute('data-subsection-id');
nextSectionId = gridRef.rows[_rowIdx].getAttribute('data-subsection-section-id');
break;
}
}
selectCellRange(nextRowId, nextColumnId, nextColumnIndex, nextSectionId, nextSubsectionId);
}
}, [flowsheetSections, selectCellRange]);
var handleKeyUp = function handleKeyUp(event) {
var key = event.keyCode;
switch (key) {
case KeyCode.KEY_SHIFT:
if (inShiftDirectionalMode.current) {
inShiftDirectionalMode.current = false;
anchorCell.current = null;
}
break;
default:
}
};
return (
/*#__PURE__*/
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
_react.default.createElement("div", {
className: cx('flowsheet-data-grid-container'),
onKeyUp: handleKeyUp
}, /*#__PURE__*/_react.default.createElement(_DataGrid.default, {
id: id,
ariaLabel: ariaLabel,
ariaLabelledBy: ariaLabelledBy,
rows: flowsheetRows,
sections: flowsheetSections,
rowHeight: rowHeight,
rowHeaderIndex: 0,
pinnedColumns: pinnedColumns,
overflowColumns: overflowColumns,
defaultColumnWidth: defaultColumnWidth,
columnHeaderHeight: columnHeaderHeight,
onCellSelect: handleCellSelection,
onSectionSelect: onSectionSelect,
onClearSelection: onClearSelectedCells,
onCellRangeSelect: handleCellRangeSelection,
hasVisibleColumnHeaders: hasVisibleColumnHeaders,
ref: dataGridFuncRef,
rowMinimumHeight: rowMinimumHeight,
isAutoFocusEnabled: isAutoFocusEnabled
}), /*#__PURE__*/_react.default.createElement(_terraVisuallyHiddenText.default, {
"aria-live": "polite",
text: cellSelectionAriaLiveMessage
}))
);
}
FlowsheetDataGrid.propTypes = propTypes;
FlowsheetDataGrid.defaultProps = defaultProps;
var _default = exports.default = (0, _reactIntl.injectIntl)(FlowsheetDataGrid);