terra-table
Version:
The Terra Table component provides user a way to display data in an accessible table format.
245 lines (207 loc) • 7.06 kB
JSX
import React, {
useContext, useRef, useState, useEffect,
} from 'react';
import classNames from 'classnames/bind';
import { injectIntl } from 'react-intl';
import * as KeyCode from 'keycode-js';
import PropTypes from 'prop-types';
import ThemeContext from 'terra-theme-context';
import ColumnContext from '../utils/ColumnContext';
import GridContext, { GridConstants } from '../utils/GridContext';
import styles from './ColumnResizeHandle.module.scss';
const cx = classNames.bind(styles);
const propTypes = {
/**
* Required string representing a unique identifier for the column resize handle.
*/
id: PropTypes.string.isRequired,
/**
* The cell's column position in the grid. This is zero based.
*/
columnIndex: PropTypes.number,
/**
* Text of the column associated with the divider.
*/
columnText: PropTypes.string.isRequired,
/**
* Number that specifies the Width of the associated column in pixels.
*/
columnWidth: PropTypes.number.isRequired,
/**
* Number that specifies the height of the resize handle in pixels.
*/
height: PropTypes.number.isRequired,
/**
* String that specifies the initial height for the resize handler to accommodate actions row.
*/
initialHeight: PropTypes.string,
/**
* Numeric increment in pixels to adjust column width when resizing via the keyboard.
*/
columnResizeIncrement: PropTypes.number,
/**
* Control is the active element
*/
isActive: PropTypes.bool,
/**
* Handler function to update isActive for parent.
*/
setIsActive: PropTypes.func,
/**
* Number that specifies the minimum column width in pixels.
*/
minimumWidth: PropTypes.number.isRequired,
/**
* Number that specifies the maximum column width in pixels.
*/
maximumWidth: PropTypes.number.isRequired,
/**
* Function that is called when onMouseDown event is triggered for the resize handle.
*/
onResizeMouseDown: PropTypes.func.isRequired,
/**
* Function that is called when onMouseDown event is triggered for the resize handle.
*/
onResizeMouseUp: PropTypes.func.isRequired,
/**
* Function that is called when the the keyboard is used to adjust the column size.
*/
onResizeHandleChange: PropTypes.func,
/**
* @private
* The intl object containing translations. This is retrieved from the context automatically by injectIntl.
*/
intl: PropTypes.shape({ formatMessage: PropTypes.func }).isRequired,
};
const defaultProps = {
columnResizeIncrement: 10,
};
const ColumnResizeHandle = (props) => {
const {
id,
columnIndex,
columnResizeIncrement,
columnText,
columnWidth,
height,
intl,
isActive,
maximumWidth,
minimumWidth,
initialHeight,
onResizeHandleChange,
onResizeMouseDown,
onResizeMouseUp,
setIsActive,
} = props;
const theme = useContext(ThemeContext);
const gridContext = useContext(GridContext);
const columnContext = useContext(ColumnContext);
const isGridContext = gridContext.role === GridConstants.GRID;
// Ref variable for native resize handle element
const resizeHandleRef = useRef();
const [isNavigationEnabled, setNavigationEnabled] = useState(true);
useEffect(() => {
if (isActive) {
resizeHandleRef.current.focus();
}
}, [isActive]);
const onMouseDown = (event) => {
// Set focus to resize handle DOM element
resizeHandleRef.current.focus();
onResizeMouseDown(event);
// Prevent event bubbling since necessary actions are handled by this component
event.stopPropagation();
// Prevent default dragging behavior
event.preventDefault();
};
const onMouseUp = () => {
onResizeMouseUp();
};
const fitToTable = () => {
// Update resize handle height to match parent table height
resizeHandleRef.current.style.height = `${height}px`;
};
const onMouseLeave = () => {
if (document.activeElement !== resizeHandleRef.current) {
resizeHandleRef.current.style.height = initialHeight || '100%';
}
};
// Handle column resize handle keyboard navigation
const onKeyDown = (event) => {
const key = event.keyCode;
switch (key) {
case KeyCode.KEY_SPACE:
case KeyCode.KEY_RETURN:
setNavigationEnabled(false);
// Lock focus into component
resizeHandleRef.current.focus();
// Assistive technologies should make announcement for focus locked
columnContext.setColumnHeaderAriaLiveMessage(intl.formatMessage({ id: 'Terra.table.cell-focus-trapped' }));
event.stopPropagation();
event.preventDefault();
break;
case KeyCode.KEY_ESCAPE:
// Release focus lock
columnContext.setColumnHeaderAriaLiveMessage(intl.formatMessage({ id: 'Terra.table.resume-navigation' }));
setNavigationEnabled(true);
break;
case KeyCode.KEY_RIGHT:
if (onResizeHandleChange && !isNavigationEnabled) {
// Increase column width
onResizeHandleChange(columnIndex, columnResizeIncrement);
}
break;
case KeyCode.KEY_LEFT:
if (onResizeHandleChange && !isNavigationEnabled) {
// Decrease column width
onResizeHandleChange(columnIndex, -columnResizeIncrement);
}
break;
default:
}
if (!isNavigationEnabled) {
event.stopPropagation();
event.preventDefault();
}
};
// Prevent click event propagation
const onClick = (event) => {
// Prevent event bubbling since necessary actions are handled by this component
event.stopPropagation();
};
const onBlur = () => {
setNavigationEnabled(true);
setIsActive(false);
};
return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-static-element-interactions
<div
id={id}
ref={resizeHandleRef}
draggable
role={isNavigationEnabled ? null : 'slider'}
tabIndex={isGridContext ? -1 : 0}
aria-hidden={isGridContext ? !isActive : false}
aria-valuemin={isActive ? minimumWidth : null}
aria-valuenow={isActive ? columnWidth : null}
aria-valuemax={isActive ? maximumWidth : null}
aria-label={isActive && isNavigationEnabled ? intl.formatMessage({ id: 'Terra.table.resize-handle-template' }, { columnText }) : null}
aria-valuetext={!isNavigationEnabled ? intl.formatMessage({ id: 'Terra.table.resize-handle-value-text' }, { columnWidth }) : null}
// eslint-disable-next-line react/forbid-dom-props
style={{ height: `${height}px` }}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseEnter={fitToTable}
onMouseLeave={onMouseLeave}
onKeyDown={onKeyDown}
onClick={onClick}
onFocus={fitToTable}
onBlur={onBlur}
className={cx('resize-handle', theme.className, { 'resize-handle-selected': !isNavigationEnabled })}
/>
);
};
ColumnResizeHandle.propTypes = propTypes;
ColumnResizeHandle.defaultProps = defaultProps;
export default injectIntl(ColumnResizeHandle);