UNPKG

terra-table

Version:

The Terra Table component provides user a way to display data in an accessible table format.

434 lines (421 loc) 17.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = _interopRequireWildcard(require("react")); var KeyCode = _interopRequireWildcard(require("keycode-js")); var _bind = _interopRequireDefault(require("classnames/bind")); var _focusTrapReact = _interopRequireDefault(require("focus-trap-react")); var _reactIntl = require("react-intl"); var _propTypes = _interopRequireDefault(require("prop-types")); var _terraThemeContext = _interopRequireDefault(require("terra-theme-context")); var _terraVisuallyHiddenText = _interopRequireDefault(require("terra-visually-hidden-text")); var _ColumnContext = _interopRequireDefault(require("../utils/ColumnContext")); var _GridContext = _interopRequireWildcard(require("../utils/GridContext")); var _focusManagement = _interopRequireDefault(require("../utils/focusManagement")); var _columnShape = require("../proptypes/columnShape"); var _CellModule = _interopRequireDefault(require("./Cell.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(_CellModule.default); var propTypes = { /** * String identifier of the row in which the Cell will be rendered. */ rowId: _propTypes.default.string.isRequired, /** * String identifier of the column in which the Cell will be rendered. */ columnId: _propTypes.default.string.isRequired, /** * The cell's row position in the table. This is zero based. */ rowIndex: _propTypes.default.number, /** * The cell's column position in the table. This is zero based. */ columnIndex: _propTypes.default.number, /** * An identifier for the section. */ sectionId: _propTypes.default.string, /** * An identifier for the subsection. */ subsectionId: _propTypes.default.string, /** * Unique identifier for the parent table */ tableId: _propTypes.default.string.isRequired, /** * Content that will be rendered within the Cell. */ children: _propTypes.default.node, /** * Boolean indicating if cell contents are masked. */ isMasked: _propTypes.default.bool, /** * Provides a custom string for masked cells to be read by screen readers. This value is only applied if the cell is masked. */ maskedLabel: _propTypes.default.string, /** * Boolean value indicating whether or not the column header is selectable. */ isSelectable: _propTypes.default.bool, /** * Boolean indicating whether the Cell is currently selected. */ isSelected: _propTypes.default.bool, /** * String that labels the cell for accessibility. */ ariaLabel: _propTypes.default.string, /** * Boolean indicating that the cell is a row header. */ isRowHeader: _propTypes.default.bool, /** * Boolean indicating that the cell has been highlighted. */ isHighlighted: _propTypes.default.bool, /** * Callback function that will be called when a cell is selected. */ onCellSelect: _propTypes.default.func, /** * String that specifies the height of the cell. Any valid CSS value is accepted. */ height: _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, /** * A zero-based index indicating which column represents the row header. * Index can be set to -1 if row headers are not required. */ rowHeaderIndex: _propTypes.default.number, /** * @private * The intl object containing translations. This is retrieved from the context automatically by injectIntl. */ intl: _propTypes.default.shape({ formatMessage: _propTypes.default.func }).isRequired, /** * @private * Id of the first row in table */ firstRowId: _propTypes.default.string, /** * @private * Id of the last row in table */ lastRowId: _propTypes.default.string, /** * @private * The color to be used for highlighting a column. */ columnHighlightColor: _propTypes.default.oneOf(Object.values(_columnShape.ColumnHighlightColor)), /** * @private * The column span index value for a column. */ columnSpanIndex: _propTypes.default.number, /** * Enables row selection capabilities for the table. * Use 'single' for single row selection and 'multiple' for multi-row selection. */ rowSelectionMode: _propTypes.default.string }; var defaultProps = { isMasked: false, isRowHeader: false, isSelectable: false, sectionId: '' }; function Cell(props) { var ariaLabel = props.ariaLabel, children = props.children, columnId = props.columnId, columnIndex = props.columnIndex, height = props.height, intl = props.intl, isHighlighted = props.isHighlighted, isMasked = props.isMasked, isRowHeader = props.isRowHeader, isSelectable = props.isSelectable, isSelected = props.isSelected, maskedLabel = props.maskedLabel, onCellSelect = props.onCellSelect, rowHeaderIndex = props.rowHeaderIndex, rowId = props.rowId, rowIndex = props.rowIndex, rowMinimumHeight = props.rowMinimumHeight, sectionId = props.sectionId, subsectionId = props.subsectionId, tableId = props.tableId, firstRowId = props.firstRowId, lastRowId = props.lastRowId, columnHighlightColor = props.columnHighlightColor, columnSpanIndex = props.columnSpanIndex, rowSelectionMode = props.rowSelectionMode; var cellRef = (0, _react.useRef)(); var theme = (0, _react.useContext)(_terraThemeContext.default); var gridContext = (0, _react.useContext)(_GridContext.default); var columnContext = (0, _react.useContext)(_ColumnContext.default); var _useState = (0, _react.useState)(false), _useState2 = (0, _slicedToArray2.default)(_useState, 2), isInteractable = _useState2[0], setIsInteractable = _useState2[1]; var _useState3 = (0, _react.useState)(false), _useState4 = (0, _slicedToArray2.default)(_useState3, 2), isFocusTrapEnabled = _useState4[0], setIsFocusTrapEnabled = _useState4[1]; var isGridContext = gridContext.role === _GridContext.GridConstants.GRID; /** * Determine if cell has focusable elements */ var hasFocusableElements = function hasFocusableElements() { var focusableElements = (0, _focusManagement.default)(cellRef.current); return focusableElements.length > 0; }; /** * Determine if a cell only has a single button or hyperlink * @returns The auto focusable button or anchor element. If there is no auto focusable element, null is returned. */ var getAutoFocusableElement = function getAutoFocusableElement() { if (!gridContext.isAutoFocusEnabled) { return null; } var focusableElements = (0, _focusManagement.default)(cellRef.current); if (focusableElements.length > 1) { return null; } return cellRef.current.querySelector('a, button'); }; /** * Determine if a cell only has a single button or hyperlink * @returns True if the element only has a single button or hyperlink. Otherwise, false. */ var hasOnlySingleButtonOrHyperlink = function hasOnlySingleButtonOrHyperlink() { return getAutoFocusableElement() !== null; }; /** * Handles the onDeactivate callback for FocusTrap component */ var deactivateFocusTrap = function deactivateFocusTrap() { setIsFocusTrapEnabled(false); if (gridContext.setCellAriaLiveMessage) { gridContext.setCellAriaLiveMessage(intl.formatMessage({ id: 'Terra.table.resume-navigation' })); } }; (0, _react.useEffect)(function () { if (isGridContext) { var autoFocusableElement = getAutoFocusableElement(); if (autoFocusableElement !== null) { // Update aria live region when auto focusable element is given focus autoFocusableElement.addEventListener('focus', function () { if (gridContext.setCellAriaLiveMessage) { gridContext.setCellAriaLiveMessage(intl.formatMessage({ id: 'Terra.table.cell-focus-trapped' })); } }); } else { setIsInteractable(hasFocusableElements()); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [gridContext, intl, isGridContext]); var handleMouseDown = function handleMouseDown(event) { if (rowSelectionMode && (event.button === 2 || hasFocusableElements())) { return; } if (!isFocusTrapEnabled) { onCellSelect({ sectionId: sectionId, subsectionId: subsectionId, rowId: rowId, rowIndex: rowIndex - 1, columnId: columnId, columnIndex: columnIndex, columnSpanIndex: columnSpanIndex, isShiftPressed: event.shiftKey, isMetaPressed: event.metaKey || event.ctrlKey, isCellSelectable: !isMasked && isSelectable }, event); } }; var handleKeyDown = function handleKeyDown(event) { var key = event.keyCode; var targetElement = event.target; if (isFocusTrapEnabled) { switch (key) { case KeyCode.KEY_ESCAPE: deactivateFocusTrap(); break; default: } event.stopPropagation(); } else { switch (key) { case KeyCode.KEY_RETURN: // Lock focus into component if (isGridContext && targetElement === cellRef.current && hasFocusableElements()) { // If the current cell has only a single button or hyperlink component, do not enable focus trap var autoFocusableElement = getAutoFocusableElement(); if (autoFocusableElement !== null) { autoFocusableElement.focus(); } else { setIsFocusTrapEnabled(true); } // Update aria live region when cell user "dives into" cell if (gridContext.setCellAriaLiveMessage) { gridContext.setCellAriaLiveMessage(intl.formatMessage({ id: 'Terra.table.cell-focus-trapped' })); } event.stopPropagation(); event.preventDefault(); } break; case KeyCode.KEY_ESCAPE: // Handle escape key event when the cell content is auto focusable if (isGridContext && targetElement !== cellRef.current && hasOnlySingleButtonOrHyperlink()) { cellRef.current.focus(); // Update aria live region when focus is returned to table cell element if (gridContext.setCellAriaLiveMessage) { gridContext.setCellAriaLiveMessage(intl.formatMessage({ id: 'Terra.table.resume-navigation' })); } } break; case KeyCode.KEY_SPACE: if (onCellSelect) { if (rowSelectionMode && hasFocusableElements()) { return; } onCellSelect({ sectionId: sectionId, subsectionId: subsectionId, rowId: rowId, rowIndex: rowIndex - 1, columnId: columnId, columnIndex: columnIndex, columnSpanIndex: columnSpanIndex, isShiftPressed: event.shiftKey, isMetaPressed: event.metaKey || event.ctrlKey, isCellSelectable: !isMasked && isSelectable }, event); } // Prevent scrolling when table cell element has focus if (['td', 'th'].indexOf(targetElement.tagName.toLowerCase()) >= 0) { event.preventDefault(); } break; default: } } }; // Create cell content for masked and blank cells var cellContent; if (isMasked) { cellContent = /*#__PURE__*/_react.default.createElement("span", { className: cx('no-data-cell', theme.className) }, maskedLabel || intl.formatMessage({ id: 'Terra.table.maskedCell' })); } else if (!children) { cellContent = /*#__PURE__*/_react.default.createElement("span", { className: cx('no-data-cell', theme.className) }, intl.formatMessage({ id: 'Terra.table.blank' })); } else { cellContent = children; } // Added to check if rowHeight is defined, it will take precedence. Otherwise the minimum row height would be used. var heightProperties = height ? { height: height } : { minHeight: rowMinimumHeight }; // eslint-disable-next-line react/forbid-dom-props var cellContentComponent = /*#__PURE__*/_react.default.createElement("div", { className: cx('cell-content', theme.className), style: _objectSpread({}, heightProperties) }, cellContent); // Render FocusTrap container when within a grid context if (isGridContext) { cellContentComponent = /*#__PURE__*/_react.default.createElement(_focusTrapReact.default, { active: isFocusTrapEnabled, focusTrapOptions: { returnFocusOnDeactivate: true, clickOutsideDeactivates: true, escapeDeactivates: false, onDeactivate: deactivateFocusTrap } }, cellContentComponent); } // Determine table cell header attribute values var cellLeftEdge = columnIndex < columnContext.pinnedColumnOffsets.length ? columnContext.pinnedColumnOffsets[columnIndex] : null; var CellTag = isRowHeader ? 'th' : 'td'; var columnHeaderId = "".concat(tableId, "-").concat(columnId, "-headerCell"); var rowHeaderId = !isRowHeader && rowHeaderIndex !== -1 ? "".concat(tableId, "-rowheader-").concat(rowId, " ") : ''; var sectionHeaderId = sectionId ? "".concat(tableId, "-").concat(sectionId, " ") : ''; var subsectionHeaderId = subsectionId ? "".concat(tableId, "-").concat(sectionId, "-").concat(subsectionId, " ") : ''; var columnHighlight = {}; if (columnHighlightColor) { columnHighlight = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, "column-highlight-".concat(columnHighlightColor.toLowerCase()), true), "first-highlight-".concat(columnHighlightColor.toLowerCase()), rowId === firstRowId), "last-highlight-".concat(columnHighlightColor.toLowerCase()), rowId === lastRowId); } var className = cx('cell', _objectSpread({ masked: isMasked, pinned: columnIndex < columnContext.pinnedColumnOffsets.length, 'last-pinned-column': columnIndex === columnContext.pinnedColumnOffsets.length - 1, selectable: isSelectable && !isMasked, selected: isSelected && !isMasked, highlighted: isHighlighted, blank: !children }, columnHighlight), theme.className); return /*#__PURE__*/_react.default.createElement(CellTag, (0, _extends2.default)({ id: isRowHeader ? "".concat(tableId, "-rowheader-").concat(rowId) : undefined, ref: isGridContext || rowSelectionMode ? cellRef : undefined, "aria-selected": isSelected || undefined, "aria-label": ariaLabel, headers: "".concat(sectionHeaderId).concat(subsectionHeaderId).concat(rowHeaderId).concat(columnHeaderId), tabIndex: isGridContext ? -1 : undefined, className: className, "data-cell-column-id": "".concat(columnId, "-").concat(columnSpanIndex), onMouseDown: onCellSelect ? handleMouseDown : undefined, onKeyDown: handleKeyDown // eslint-disable-next-line react/forbid-component-props , style: { left: cellLeftEdge } }, isRowHeader && { scope: 'row', role: 'rowheader' }), cellContentComponent, isInteractable && /*#__PURE__*/_react.default.createElement(_terraVisuallyHiddenText.default, { text: intl.formatMessage({ id: 'Terra.table.cell-interactable' }) })); } Cell.propTypes = propTypes; Cell.defaultProps = defaultProps; var _default = exports.default = /*#__PURE__*/_react.default.memo((0, _reactIntl.injectIntl)(Cell));