terra-table
Version:
The Terra Table component provides user a way to display data in an accessible table format.
458 lines (445 loc) • 18.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
});
exports.default = void 0;
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _react = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _reactIntl = require("react-intl");
var KeyCode = _interopRequireWildcard(require("keycode-js"));
var _bind = _interopRequireDefault(require("classnames/bind"));
var _terraThemeContext = _interopRequireDefault(require("terra-theme-context"));
var _terraVisuallyHiddenText = _interopRequireDefault(require("terra-visually-hidden-text"));
var _terraButton = _interopRequireDefault(require("terra-button"));
var _terraIcon = require("terra-icon");
var _validators = require("../proptypes/validators");
var _ColumnResizeHandle = _interopRequireDefault(require("./ColumnResizeHandle"));
var _GridContext = _interopRequireWildcard(require("../utils/GridContext"));
var _columnShape = require("../proptypes/columnShape");
var _ColumnContext = _interopRequireDefault(require("../utils/ColumnContext"));
var _ColumnHeaderCellModule = _interopRequireDefault(require("./ColumnHeaderCell.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; }
/* eslint-disable react-hooks/exhaustive-deps */
var cx = _bind.default.bind(_ColumnHeaderCellModule.default);
var propTypes = {
/**
* Required string representing a unique identifier for the column header cell.
*/
id: _propTypes.default.string.isRequired,
/**
* Unique identifier for the parent table
*/
tableId: _propTypes.default.string.isRequired,
/**
* Unique identifier for the column
*/
columnId: _propTypes.default.string.isRequired,
/**
* CallBack to trigger re-focusing when focused row or col didn't change, but focus update is needed
*/
triggerFocus: _propTypes.default.func,
/**
* String of text to render within the column header cell.
*/
displayName: _propTypes.default.string,
/**
* A string indicating which sorting indicator should be rendered. If not provided, no sorting indicator will be rendered.
* If a `component` value is specified, `sortIndicator` will be ignored. One of `ascending`, `descending`.
*/
sortIndicator: _propTypes.default.oneOf(Object.values(_columnShape.SortIndicators)),
/**
* Boolean value indicating whether or not the column has an error in the data.
*/
hasError: _propTypes.default.bool,
/**
* Number that specifies the minimum column width in pixels.
*/
minimumWidth: _propTypes.default.number,
/**
* Number that specifies the maximum column width in pixels.
*/
maximumWidth: _propTypes.default.number,
/**
* Boolean value indicating whether or not the header cell is focused.
*/
isActive: _propTypes.default.bool,
/**
* Boolean that specifies that header cell owns a resize handle.
*/
ownsResizeHandle: _propTypes.default.bool,
/**
* Boolean value indicating whether or not the header cell text is displayed in the cell.
*/
isDisplayVisible: _propTypes.default.bool,
/**
* Boolean value indicating whether or not the column header is selectable.
*/
isSelectable: _propTypes.default.bool,
/**
* Boolean value indicating whether or not the column header cell is an action cell.
* The action cell might be a placeholder cell without actual action button
*/
isActionCell: _propTypes.default.bool,
/**
* Data for action cell.
*/
action: _validators.validateAction,
/**
* Boolean value indicating whether or not the column header is resizable.
*/
isResizable: _propTypes.default.bool,
/**
* Boolean value indicating whether or not the column resize handle is active.
*/
isResizeHandleActive: _propTypes.default.bool,
/**
* A function to be executed upon the resize handler activation to pass its data to parent component.
* @param {element} leftNeighborCell - `columnHeaderCellRef.current`
* Skip both parameters to indicate that there is no active resize handle at the moment.
*/
resizeHandleStateSetter: _propTypes.default.func,
/**
* String that specifies the initial height for the resize handler to accommodate actions row.
*/
initialHeight: _propTypes.default.string,
/**
* Height of the parent table.
*/
tableHeight: _propTypes.default.number,
/**
* Boolean value indicating whether or not the column header is resizable.
*/
isResizeActive: _propTypes.default.bool,
/**
* Numeric increment in pixels to adjust column width when resizing via the keyboard.
*/
columnResizeIncrement: _propTypes.default.number,
/**
* The number (in px) specifying the width of the column.
*/
width: _propTypes.default.number,
/**
* String that specifies the column height. Any valid CSS height value accepted.
*/
headerHeight: _propTypes.default.string.isRequired,
/**
* The cell's column position in the grid. This is zero based.
*/
columnIndex: _propTypes.default.number,
/**
* The column span value for a column.
*/
columnSpan: _propTypes.default.number,
/**
* Function that is called when a selectable header cell is selected. Parameters:
* @param {string} rowId rowId
* @param {string} columnId columnId
*/
onColumnSelect: _propTypes.default.func,
/**
* Function that is called when the mouse down event is triggered on the column resize handle.
*/
onResizeMouseDown: _propTypes.default.func,
/**
* Function that is called when the the keyboard is used to adjust the column size.
*/
onResizeHandleChange: _propTypes.default.func,
/**
* @private
* Object containing intl APIs
*/
intl: _propTypes.default.shape({
formatMessage: _propTypes.default.func
}),
/**
* @private
* The information to be conveyed to screen readers about the highlighted column.
*/
columnHighlightDescription: _propTypes.default.string,
/**
* @private
* The color to be used for highlighting a column.
*/
columnHighlightColor: _propTypes.default.oneOf(Object.values(_columnShape.ColumnHighlightColor))
};
var defaultProps = {
hasError: false,
isSelectable: false,
isActive: false,
isDisplayVisible: true,
isResizable: false,
isResizeActive: false
};
var ColumnHeaderCell = function ColumnHeaderCell(props) {
var id = props.id,
tableId = props.tableId,
isActionCell = props.isActionCell,
action = props.action,
displayName = props.displayName,
sortIndicator = props.sortIndicator,
hasError = props.hasError,
isActive = props.isActive,
isDisplayVisible = props.isDisplayVisible,
isSelectable = props.isSelectable,
isResizable = props.isResizable,
isResizeHandleActive = props.isResizeHandleActive,
resizeHandleStateSetter = props.resizeHandleStateSetter,
initialHeight = props.initialHeight,
triggerFocus = props.triggerFocus,
columnId = props.columnId,
tableHeight = props.tableHeight,
isResizeActive = props.isResizeActive,
columnResizeIncrement = props.columnResizeIncrement,
width = props.width,
minimumWidth = props.minimumWidth,
maximumWidth = props.maximumWidth,
headerHeight = props.headerHeight,
onColumnSelect = props.onColumnSelect,
intl = props.intl,
columnIndex = props.columnIndex,
columnSpan = props.columnSpan,
onResizeMouseDown = props.onResizeMouseDown,
onResizeHandleChange = props.onResizeHandleChange,
ownsResizeHandle = props.ownsResizeHandle,
columnHighlightDescription = props.columnHighlightDescription,
columnHighlightColor = props.columnHighlightColor;
var columnContext = (0, _react.useContext)(_ColumnContext.default);
var gridContext = (0, _react.useContext)(_GridContext.default);
var columnHeaderCellRef = (0, _react.useRef)();
var setResizeHandleActive = (0, _react.useCallback)(function (setActive) {
if (setActive) {
resizeHandleStateSetter(columnId);
} else {
resizeHandleStateSetter();
}
}, [columnId, resizeHandleStateSetter]);
var isGridContext = gridContext.role === _GridContext.GridConstants.GRID;
(0, _react.useEffect)(function () {
if (isActive) {
if (isResizable && isResizeActive) {
setResizeHandleActive(true);
} else {
columnHeaderCellRef.current.focus();
setResizeHandleActive(false);
}
} else {
setResizeHandleActive(false);
}
}, [isActive, isResizable, isResizeActive]);
var onResizeHandleMouseDown = (0, _react.useCallback)(function (event) {
event.stopPropagation();
if (onResizeMouseDown) {
onResizeMouseDown(event, columnIndex, columnHeaderCellRef.current.offsetWidth);
}
}, [columnIndex, onResizeMouseDown]);
// Restore focus to column header after resize action is completed.
var onResizeHandleMouseUp = (0, _react.useCallback)(function () {
columnHeaderCellRef.current.focus();
setResizeHandleActive(false);
}, []);
// Handle column header selection via the mouse click.
var handleMouseDown = function handleMouseDown() {
onColumnSelect(columnId);
};
// Handle column header selection via the space bar.
var handleKeyDown = function handleKeyDown(event, callback) {
var key = event.keyCode;
switch (key) {
case KeyCode.KEY_SPACE:
case KeyCode.KEY_RETURN:
if (callback) {
// for action button
callback();
break;
} else if (isSelectable && onColumnSelect) {
onColumnSelect(columnId);
}
event.stopPropagation();
event.preventDefault(); // prevent the default scrolling
break;
case KeyCode.KEY_LEFT:
if (isResizable && isResizeHandleActive && isGridContext) {
setResizeHandleActive(false);
if (triggerFocus) {
triggerFocus();
}
event.stopPropagation();
event.preventDefault();
}
break;
case KeyCode.KEY_RIGHT:
if (isResizable && !isResizeHandleActive && isGridContext) {
setResizeHandleActive(true);
event.stopPropagation();
event.preventDefault();
}
break;
default:
}
};
var errorIcon = hasError && /*#__PURE__*/_react.default.createElement(_terraIcon.IconError, {
className: cx('error-icon')
});
// Add the sort indicator based on the sort direction
var sortIndicatorIcon;
var sortDescription = '';
if (sortIndicator === _columnShape.SortIndicators.ASCENDING) {
sortIndicatorIcon = /*#__PURE__*/_react.default.createElement(_terraIcon.IconUp, null);
sortDescription = intl.formatMessage({
id: 'Terra.table.sort-ascending'
});
} else if (sortIndicator === _columnShape.SortIndicators.DESCENDING) {
sortIndicatorIcon = /*#__PURE__*/_react.default.createElement(_terraIcon.IconDown, null);
sortDescription = intl.formatMessage({
id: 'Terra.table.sort-descending'
});
}
// Add column highlight indicator based on color
var columnHighlightIcon;
if (columnHighlightColor === _columnShape.ColumnHighlightColor.GREEN) {
columnHighlightIcon = /*#__PURE__*/_react.default.createElement("svg", {
className: cx('highlight-icon-svg'),
xmlns: "http://www.w3.org/2000/svg"
}, /*#__PURE__*/_react.default.createElement("circle", {
className: cx('highlight-icon-circle'),
r: "3",
cx: "110%",
cy: "11",
transform: "translate(-5)"
}));
} else if (columnHighlightColor === _columnShape.ColumnHighlightColor.ORANGE) {
columnHighlightIcon = /*#__PURE__*/_react.default.createElement("svg", {
className: cx('highlight-icon-svg'),
xmlns: "http://www.w3.org/2000/svg"
}, /*#__PURE__*/_react.default.createElement("rect", {
className: cx('highlight-icon-square'),
x: "110%",
y: "7.5",
transform: "translate(-8)"
}));
}
// Retrieve current theme from context
var theme = (0, _react.useContext)(_terraThemeContext.default);
// Calculate cell left position for pinned columns due to their sticky position style
var cellLeftEdge = columnIndex < columnContext.pinnedColumnHeaderOffsets.length ? columnContext.pinnedColumnHeaderOffsets[columnIndex] : null;
// For tables, we want elements to be tabbable when selectable, but not anytime else.
var buttonTabIndex = isSelectable ? 0 : undefined;
// Determine if button element is required for column header
var hasButtonElement = isSelectable && displayName || isActionCell && action;
var cellTabIndex;
if (isGridContext) {
if (columnIndex === 0 && !isActionCell) {
buttonTabIndex = isSelectable && displayName ? 0 : undefined;
cellTabIndex = !hasButtonElement ? 0 : undefined;
} else {
// For grids, we only want 1 tab stop. We then define the focus behavior in DataGrid.
buttonTabIndex = isSelectable && displayName ? -1 : undefined;
cellTabIndex = !hasButtonElement ? -1 : undefined;
}
}
// Format header description for screenreader
var headerDescription = displayName;
headerDescription += errorIcon ? ", ".concat(intl.formatMessage({
id: 'Terra.table.columnError'
})) : '';
headerDescription += sortDescription ? ", ".concat(sortDescription) : '';
headerDescription += columnHighlightDescription ? ", ".concat(columnHighlightDescription) : '';
var isPinnedColumn = columnIndex < columnContext.pinnedColumnHeaderOffsets.length;
var CellTag = !isActionCell ? 'th' : 'td';
var setColumnHeaderCellRef = function setColumnHeaderCellRef(node) {
columnHeaderCellRef.current = node;
};
// Create cell content
var cellContent;
if (isActionCell) {
if (action) {
cellContent = /*#__PURE__*/_react.default.createElement(_terraButton.default, {
variant: "de-emphasis",
isCompact: true,
refCallback: setColumnHeaderCellRef,
onClick: action.onClick,
onKeyDown: function onKeyDown(event) {
return handleKeyDown(event, action === null || action === void 0 ? void 0 : action.onClick);
},
text: action.label
});
} else {
cellContent = /*#__PURE__*/_react.default.createElement("span", {
className: cx('display-text', 'hidden')
}, intl.formatMessage({
id: 'Terra.table.noAction'
}));
}
} else {
cellContent = /*#__PURE__*/_react.default.createElement("div", (0, _extends2.default)({
className: cx('header-container')
}, hasButtonElement && {
ref: columnHeaderCellRef,
role: 'button'
}, {
tabIndex: buttonTabIndex
}), errorIcon, /*#__PURE__*/_react.default.createElement("span", {
"aria-hidden": true,
className: cx('display-text', {
hidden: !isDisplayVisible
})
}, displayName), sortIndicatorIcon, /*#__PURE__*/_react.default.createElement(_terraVisuallyHiddenText.default, {
text: headerDescription
}), columnHighlightIcon);
}
var resizeHandleId = "".concat(tableId, "-").concat(columnId, "-resizeHandle");
return (
/*#__PURE__*/
/* eslint-disable react/forbid-dom-props */
_react.default.createElement(CellTag, {
ref: !hasButtonElement ? columnHeaderCellRef : undefined,
id: "".concat(tableId, "-").concat(id),
key: id,
className: cx('column-header', theme.className, {
'action-cell': isActionCell,
selectable: isSelectable,
pinned: isPinnedColumn,
'last-pinned-column': columnIndex === columnContext.pinnedColumnHeaderOffsets.length - 1
}),
tabIndex: cellTabIndex,
role: !isActionCell ? 'columnheader' : undefined,
scope: !isActionCell ? 'col' : undefined
// action Cell has to own a corresponding resize handle to avoid a double announcement on handle focus
,
"aria-owns": ownsResizeHandle ? resizeHandleId : undefined,
title: !isActionCell ? displayName : action === null || action === void 0 ? void 0 : action.label,
colSpan: columnSpan,
onMouseDown: isSelectable && onColumnSelect ? handleMouseDown : undefined,
onKeyDown: isSelectable || isResizable ? handleKeyDown : undefined
// eslint-disable-next-line react/forbid-component-props
,
style: {
height: isActionCell ? 'auto' : headerHeight,
left: cellLeftEdge,
top: isActionCell ? headerHeight : undefined
}
}, cellContent, isResizable && !isActionCell && /*#__PURE__*/_react.default.createElement(_ColumnResizeHandle.default, {
id: resizeHandleId,
columnIndex: columnIndex,
columnText: displayName,
columnWidth: width,
columnResizeIncrement: columnResizeIncrement,
isActive: isResizeHandleActive,
setIsActive: setResizeHandleActive,
height: tableHeight,
initialHeight: initialHeight,
minimumWidth: minimumWidth,
maximumWidth: maximumWidth,
onResizeMouseDown: onResizeHandleMouseDown,
onResizeMouseUp: onResizeHandleMouseUp,
onResizeHandleChange: onResizeHandleChange
}))
);
};
ColumnHeaderCell.propTypes = propTypes;
ColumnHeaderCell.defaultProps = defaultProps;
var _default = exports.default = /*#__PURE__*/_react.default.memo((0, _reactIntl.injectIntl)(ColumnHeaderCell));