UNPKG

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
"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);