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