@gechiui/components
Version:
UI components for GeChiUI.
219 lines (174 loc) • 7.75 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import { createElement } from "@gechiui/element";
/**
* External dependencies
*/
import { includes } from 'lodash';
/**
* GeChiUI dependencies
*/
import { focus } from '@gechiui/dom';
import { forwardRef, useCallback } from '@gechiui/element';
import { UP, DOWN, LEFT, RIGHT } from '@gechiui/keycodes';
/**
* Internal dependencies
*/
import RovingTabIndexContainer from './roving-tab-index';
/**
* Return focusables in a row element, excluding those from other branches
* nested within the row.
*
* @param {Element} rowElement The DOM element representing the row.
*
* @return {?Array} The array of focusables in the row.
*/
function getRowFocusables(rowElement) {
const focusablesInRow = focus.focusable.find(rowElement, {
sequential: true
});
if (!focusablesInRow || !focusablesInRow.length) {
return;
}
return focusablesInRow.filter(focusable => {
return focusable.closest('[role="row"]') === rowElement;
});
}
/**
* Renders both a table and tbody element, used to create a tree hierarchy.
*
* @see https://github.com/GeChiUI/gutenberg/blob/HEAD/packages/components/src/tree-grid/README.md
* @param {Object} props Component props.
* @param {GCElement} props.children Children to be rendered.
* @param {Function} props.onExpandRow Callback to fire when row is expanded.
* @param {Function} props.onCollapseRow Callback to fire when row is collapsed.
* @param {Object} ref A ref to the underlying DOM table element.
*/
function TreeGrid(_ref, ref) {
let {
children,
onExpandRow = () => {},
onCollapseRow = () => {},
...props
} = _ref;
const onKeyDown = useCallback(event => {
const {
keyCode,
metaKey,
ctrlKey,
altKey,
shiftKey
} = event;
const hasModifierKeyPressed = metaKey || ctrlKey || altKey || shiftKey;
if (hasModifierKeyPressed || !includes([UP, DOWN, LEFT, RIGHT], keyCode)) {
return;
} // The event will be handled, stop propagation.
event.stopPropagation();
const {
activeElement
} = document;
const {
currentTarget: treeGridElement
} = event;
if (!treeGridElement.contains(activeElement)) {
return;
} // Calculate the columnIndex of the active element.
const activeRow = activeElement.closest('[role="row"]');
const focusablesInRow = getRowFocusables(activeRow);
const currentColumnIndex = focusablesInRow.indexOf(activeElement);
if (includes([LEFT, RIGHT], keyCode)) {
// Calculate to the next element.
let nextIndex;
if (keyCode === LEFT) {
nextIndex = Math.max(0, currentColumnIndex - 1);
} else {
nextIndex = Math.min(currentColumnIndex + 1, focusablesInRow.length - 1);
} // Focus is either at the left or right edge of the grid.
if (nextIndex === currentColumnIndex) {
if (keyCode === LEFT) {
var _activeRow$ariaLevel, _getRowFocusables, _getRowFocusables$;
// Left:
// If a row is focused, and it is expanded, collapses the current row.
if (activeRow.getAttribute('aria-expanded') === 'true') {
onCollapseRow(activeRow);
event.preventDefault();
return;
} // If a row is focused, and it is collapsed, moves to the parent row (if there is one).
const level = Math.max(parseInt((_activeRow$ariaLevel = activeRow === null || activeRow === void 0 ? void 0 : activeRow.ariaLevel) !== null && _activeRow$ariaLevel !== void 0 ? _activeRow$ariaLevel : 1, 10) - 1, 1);
const rows = Array.from(treeGridElement.querySelectorAll('[role="row"]'));
let parentRow = activeRow;
const currentRowIndex = rows.indexOf(activeRow);
for (let i = currentRowIndex; i >= 0; i--) {
if (parseInt(rows[i].ariaLevel, 10) === level) {
parentRow = rows[i];
break;
}
}
(_getRowFocusables = getRowFocusables(parentRow)) === null || _getRowFocusables === void 0 ? void 0 : (_getRowFocusables$ = _getRowFocusables[0]) === null || _getRowFocusables$ === void 0 ? void 0 : _getRowFocusables$.focus();
}
if (keyCode === RIGHT) {
// Right:
// If a row is focused, and it is collapsed, expands the current row.
if (activeRow.getAttribute('aria-expanded') === 'false') {
onExpandRow(activeRow);
event.preventDefault();
return;
} // If a row is focused, and it is expanded, focuses the rightmost cell in the row.
const focusableItems = getRowFocusables(activeRow);
if (focusableItems.length > 0) {
var _focusableItems;
(_focusableItems = focusableItems[focusableItems.length - 1]) === null || _focusableItems === void 0 ? void 0 : _focusableItems.focus();
}
} // Prevent key use for anything else. For example, Voiceover
// will start reading text on continued use of left/right arrow
// keys.
event.preventDefault();
return;
} // Focus the next element.
focusablesInRow[nextIndex].focus(); // Prevent key use for anything else. This ensures Voiceover
// doesn't try to handle key navigation.
event.preventDefault();
} else if (includes([UP, DOWN], keyCode)) {
// Calculate the rowIndex of the next row.
const rows = Array.from(treeGridElement.querySelectorAll('[role="row"]'));
const currentRowIndex = rows.indexOf(activeRow);
let nextRowIndex;
if (keyCode === UP) {
nextRowIndex = Math.max(0, currentRowIndex - 1);
} else {
nextRowIndex = Math.min(currentRowIndex + 1, rows.length - 1);
} // Focus is either at the top or bottom edge of the grid. Do nothing.
if (nextRowIndex === currentRowIndex) {
// Prevent key use for anything else. For example, Voiceover
// will start navigating horizontally when reaching the vertical
// bounds of a table.
event.preventDefault();
return;
} // Get the focusables in the next row.
const focusablesInNextRow = getRowFocusables(rows[nextRowIndex]); // If for some reason there are no focusables in the next row, do nothing.
if (!focusablesInNextRow || !focusablesInNextRow.length) {
// Prevent key use for anything else. For example, Voiceover
// will still focus text when using arrow keys, while this
// component should limit navigation to focusables.
event.preventDefault();
return;
} // Try to focus the element in the next row that's at a similar column to the activeElement.
const nextIndex = Math.min(currentColumnIndex, focusablesInNextRow.length - 1);
focusablesInNextRow[nextIndex].focus(); // Prevent key use for anything else. This ensures Voiceover
// doesn't try to handle key navigation.
event.preventDefault();
}
}, [onExpandRow, onCollapseRow]);
/* Disable reason: A treegrid is implemented using a table element. */
/* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role */
return createElement(RovingTabIndexContainer, null, createElement("table", _extends({}, props, {
role: "treegrid",
onKeyDown: onKeyDown,
ref: ref
}), createElement("tbody", null, children)));
/* eslint-enable jsx-a11y/no-noninteractive-element-to-interactive-role */
}
export default forwardRef(TreeGrid);
export { default as TreeGridRow } from './row';
export { default as TreeGridCell } from './cell';
export { default as TreeGridItem } from './item';
//# sourceMappingURL=index.js.map