UNPKG

terra-table

Version:

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

218 lines (194 loc) 7.5 kB
/* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useCallback, useEffect, useRef, } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames/bind'; import ResizeObserver from 'resize-observer-polyfill'; import ColumnHeaderCell from './ColumnHeaderCell'; import columnShape from '../proptypes/columnShape'; import styles from './ColumnHeader.module.scss'; const cx = classNames.bind(styles); const propTypes = { /** * Unique identifier for the parent table */ tableId: PropTypes.string.isRequired, /** * Data for columns. By default, columns will be presented in the order given. */ columns: PropTypes.arrayOf(columnShape).isRequired, /** * String that specifies the column header height. Any valid CSS height value accepted. */ headerHeight: PropTypes.string.isRequired, /** * Number that specifies the height of the table in pixels. */ tableHeight: PropTypes.number, /** * Column index for cell that can receive tab focus. */ activeColumnIndex: PropTypes.number, /** * Row index for cell that can receive tab focus. */ focusedRowIndex: PropTypes.number, /** * CallBack to trigger re-focusing when focused row or col didn't change, but focus update is needed */ triggerFocus: PropTypes.func, /** * Specifies if resize handle should be active. */ isActiveColumnResizing: PropTypes.bool, /** * Numeric increment in pixels to adjust column width when resizing via the keyboard. */ columnResizeIncrement: PropTypes.number, /** * Function that is called when a selectable header cell is selected. Parameters: * @param {string} columnId columnId */ onColumnSelect: PropTypes.func, /** * Function that is called when the mouse down event is triggered on the column resize handle. */ onResizeMouseDown: PropTypes.func, /** * Function that is called when the the keyboard is used to adjust the column size. */ onResizeHandleChange: PropTypes.func, /** * Boolean indicating whether or not the table columns should be displayed. */ hasVisibleColumnHeaders: PropTypes.bool, /** * A Boolean value specifying whether the table has actions in column headers. */ hasColumnHeaderActions: PropTypes.bool, }; const defaultProps = { hasVisibleColumnHeaders: true, }; const ColumnHeader = (props) => { const { tableId, activeColumnIndex, focusedRowIndex, triggerFocus, isActiveColumnResizing, columnResizeIncrement, columns, headerHeight, tableHeight, onColumnSelect, onResizeMouseDown, onResizeHandleChange, hasVisibleColumnHeaders, hasColumnHeaderActions, } = props; // Header container height observer --------------- const headerRef = useRef(); const [headerContainerHeight, setHeaderContainerHeight] = useState(0); useEffect(() => { const resizeObserver = new ResizeObserver(() => { const heightOffset = hasColumnHeaderActions ? 2 : 1; // needs 2 pixels if actions row exists in headers to avoid scroll setHeaderContainerHeight(headerRef.current.offsetHeight - heightOffset); }); resizeObserver.observe(headerRef.current); return () => { resizeObserver.disconnect(); }; }, [headerRef]); // Active resize handle management ------------------- // The active column and neighbour cell have to be known to both header and action cells as they share resize handle. const [activeResizeHandlerColumnId, setActiveResizeHandlerColumnId] = useState(); const resizeHandleStateSetter = useCallback((columnId) => { if (columnId !== activeResizeHandlerColumnId) { setActiveResizeHandlerColumnId(columnId); } }, [activeResizeHandlerColumnId]); // Is needed to adjust the header column resize handler to accommodate actions header height const initialHeight = hasColumnHeaderActions ? `${headerContainerHeight}px` : undefined; return ( <thead ref={headerRef}> <tr aria-rowindex={1} data-row-id={`${tableId}-header-row`} className={cx('column-header-row', { hidden: !hasVisibleColumnHeaders })} height={hasVisibleColumnHeaders ? headerHeight : undefined} > {columns.map((column, columnIndex) => ( <ColumnHeaderCell key={`${column.id}-headerCell`} id={`${column.id}-headerCell`} tableId={tableId} columnId={column.id} columnIndex={columnIndex} columnSpan={column.columnSpan} displayName={column.displayName} isDisplayVisible={column.isDisplayVisible} width={column.width} minimumWidth={column.minimumWidth} maximumWidth={column.maximumWidth} headerHeight={headerHeight} isResizable={hasVisibleColumnHeaders && column.isResizable} initialHeight={initialHeight} isResizeHandleActive={activeResizeHandlerColumnId === column.id} resizeHandleStateSetter={resizeHandleStateSetter} isSelectable={hasVisibleColumnHeaders && column.isSelectable} tableHeight={tableHeight} triggerFocus={triggerFocus} isActive={activeColumnIndex === columnIndex && focusedRowIndex === 0} // can be 2 rows in header isResizeActive={activeColumnIndex === columnIndex && isActiveColumnResizing} columnResizeIncrement={columnResizeIncrement} hasError={column.hasError} sortIndicator={column.sortIndicator} onColumnSelect={onColumnSelect} onResizeMouseDown={onResizeMouseDown} onResizeHandleChange={onResizeHandleChange} columnHighlightColor={column.columnHighlightColor} columnHighlightDescription={column.columnHighlightDescription} /> ))} </tr> {/* Actions row */} {hasColumnHeaderActions && hasVisibleColumnHeaders && ( <tr aria-rowindex={2} data-row-id={`${tableId}-header-actions-row`} className={cx('column-actions-row', { hidden: !hasVisibleColumnHeaders })} > {columns.map((column, columnIndex) => ( <ColumnHeaderCell key={`${column.id}-actionCell`} id={`${column.id}-actionCell`} tableId={tableId} columnId={column.id} isActionCell action={column.action} columnIndex={columnIndex} columnSpan={column.columnSpan} isDisplayVisible={column.isDisplayVisible} width={column.isResizable && column.width} minimumWidth={column.minimumWidth} maximumWidth={column.maximumWidth} headerHeight={headerHeight} isResizable={hasVisibleColumnHeaders && column.isResizable} ownsResizeHandle={focusedRowIndex === 1} isResizeHandleActive={activeResizeHandlerColumnId === column.id} resizeHandleStateSetter={resizeHandleStateSetter} // does not need isSelectable prop for actions row isActive={activeColumnIndex === columnIndex && focusedRowIndex === 1} isResizeActive={activeColumnIndex === columnIndex && isActiveColumnResizing} /> ))} </tr> )} </thead> ); }; ColumnHeader.propTypes = propTypes; ColumnHeader.defaultProps = defaultProps; export default React.memo(ColumnHeader);