@elastic/eui
Version:
Elastic UI Component Library
246 lines (241 loc) • 11 kB
JavaScript
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createContext, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { tabbable } from 'tabbable';
import { keys } from '../../../services';
export var DataGridFocusContext = /*#__PURE__*/createContext({
focusedCell: undefined,
setFocusedCell: function setFocusedCell() {},
setIsFocusedCellInView: function setIsFocusedCellInView() {},
onFocusUpdate: function onFocusUpdate() {
return function () {};
},
focusFirstVisibleInteractiveCell: function focusFirstVisibleInteractiveCell() {}
});
/**
* Main focus context and overarching focus state management
*/
export var useFocus = function useFocus() {
// Maintain a map of focus cell state callbacks
var cellsUpdateFocus = useRef(new Map());
var onFocusUpdate = useCallback(function (cell, updateFocus) {
var key = "".concat(cell[0], "-").concat(cell[1]);
cellsUpdateFocus.current.set(key, updateFocus);
return function () {
cellsUpdateFocus.current.delete(key);
};
}, []);
// Current focused cell
var _useState = useState(false),
_useState2 = _slicedToArray(_useState, 2),
isFocusedCellInView = _useState2[0],
setIsFocusedCellInView = _useState2[1];
var _useState3 = useState(undefined),
_useState4 = _slicedToArray(_useState3, 2),
focusedCell = _useState4[0],
_setFocusedCell = _useState4[1];
var setFocusedCell = useCallback(function (nextFocusedCell, forceUpdate) {
_setFocusedCell(function (prevFocusedCell) {
// If the x/y coordinates remained the same, don't update. This keeps the focusedCell
// reference stable, and allows it to be used in places that need reference equality.
if (!forceUpdate && nextFocusedCell[0] === (prevFocusedCell === null || prevFocusedCell === void 0 ? void 0 : prevFocusedCell[0]) && nextFocusedCell[1] === (prevFocusedCell === null || prevFocusedCell === void 0 ? void 0 : prevFocusedCell[1])) {
return prevFocusedCell;
} else {
setIsFocusedCellInView(true); // scrolling.ts ensures focused cells are fully in view
return nextFocusedCell;
}
});
}, []);
var previousCell = useRef(undefined);
useLayoutEffect(function () {
if (previousCell.current) {
notifyCellOfFocusState(cellsUpdateFocus.current, previousCell.current, false);
}
previousCell.current = focusedCell;
if (focusedCell) {
notifyCellOfFocusState(cellsUpdateFocus.current, focusedCell, true);
}
}, [cellsUpdateFocus, focusedCell]);
var focusFirstVisibleInteractiveCell = useCallback(function () {
setFocusedCell([0, -1]);
}, [setFocusedCell]);
var focusProps = useMemo(function () {
return isFocusedCellInView ? {
// FireFox allows tabbing to a div that is scrollable, while Chrome does not
tabIndex: -1
} : {
tabIndex: 0,
onKeyUp: function onKeyUp(e) {
// Ensure we only manually focus into the grid via keyboard tab -
// mouse users can accidentally trigger focus by clicking on scrollbars
if (e.key === keys.TAB) {
// if e.target (the source element of the `focus event`) matches
// e.currentTarget (always the div with this onKeyUp listener)
// then the user has focused directly on the data grid wrapper
if (e.target === e.currentTarget) {
focusFirstVisibleInteractiveCell();
}
}
}
};
}, [isFocusedCellInView, focusFirstVisibleInteractiveCell]);
return useMemo(function () {
return {
onFocusUpdate: onFocusUpdate,
focusedCell: focusedCell,
setFocusedCell: setFocusedCell,
setIsFocusedCellInView: setIsFocusedCellInView,
focusFirstVisibleInteractiveCell: focusFirstVisibleInteractiveCell,
focusProps: focusProps
};
}, [onFocusUpdate, focusedCell, setFocusedCell, setIsFocusedCellInView, focusFirstVisibleInteractiveCell, focusProps]);
};
export var notifyCellOfFocusState = function notifyCellOfFocusState(cellsUpdateFocus, cell, isFocused) {
var key = "".concat(cell[0], "-").concat(cell[1]);
var onFocus = cellsUpdateFocus.get(key);
if (onFocus) {
onFocus(isFocused);
}
};
/**
* Keydown handler for connecting focus state with keyboard navigation
*/
export var createKeyDownHandler = function createKeyDownHandler(_ref) {
var gridElement = _ref.gridElement,
visibleColCount = _ref.visibleColCount,
visibleRowCount = _ref.visibleRowCount,
visibleRowStartIndex = _ref.visibleRowStartIndex,
rowCount = _ref.rowCount,
pagination = _ref.pagination,
hasFooter = _ref.hasFooter,
focusContext = _ref.focusContext;
return function (event) {
var focusedCell = focusContext.focusedCell,
setFocusedCell = focusContext.setFocusedCell;
if (focusedCell == null) return;
if (gridElement == null || !gridElement.contains(document.activeElement)) {
// if the `contentElement` does not contain the focused element, don't handle the event
// this happens when React bubbles the key event through a portal
return;
}
var _focusedCell = _slicedToArray(focusedCell, 2),
x = _focusedCell[0],
y = _focusedCell[1];
var key = event.key,
ctrlKey = event.ctrlKey;
if (key === keys.ARROW_DOWN) {
event.preventDefault();
if (hasFooter ? y < visibleRowCount : y < visibleRowCount - 1) {
if (y === -1) {
// The header is sticky, so on scrolling virtualized grids, row 0 will not
// always be rendered to navigate down to. We need to account for this by
// sending the down arrow to the first visible/virtualized row instead
setFocusedCell([x, visibleRowStartIndex]);
} else {
setFocusedCell([x, y + 1]);
}
}
} else if (key === keys.ARROW_LEFT) {
event.preventDefault();
if (x > 0) {
setFocusedCell([x - 1, y]);
}
} else if (key === keys.ARROW_UP) {
event.preventDefault();
if (y > -1) {
setFocusedCell([x, y - 1]);
}
} else if (key === keys.ARROW_RIGHT) {
event.preventDefault();
if (x < visibleColCount - 1) {
setFocusedCell([x + 1, y]);
}
} else if (key === keys.PAGE_DOWN) {
if (pagination && pagination.pageSize > 0) {
event.preventDefault();
var pageSize = pagination.pageSize;
var pageCount = Math.ceil(rowCount / pageSize);
var pageIndex = pagination.pageIndex;
if (pageIndex < pageCount - 1) {
pagination.onChangePage(pageIndex + 1);
}
setFocusedCell([focusedCell[0], 0]);
}
} else if (key === keys.PAGE_UP) {
if (pagination && pagination.pageSize > 0) {
event.preventDefault();
var _pageIndex = pagination.pageIndex;
if (_pageIndex > 0) {
pagination.onChangePage(_pageIndex - 1);
}
setFocusedCell([focusedCell[0], pagination.pageSize - 1]);
}
} else if (key === (ctrlKey && keys.END)) {
event.preventDefault();
setFocusedCell([visibleColCount - 1, visibleRowCount - 1]);
} else if (key === (ctrlKey && keys.HOME)) {
event.preventDefault();
setFocusedCell([0, 0]);
} else if (key === keys.END) {
event.preventDefault();
setFocusedCell([visibleColCount - 1, y]);
} else if (key === keys.HOME) {
event.preventDefault();
setFocusedCell([0, y]);
}
};
};
/**
* Mutation observer for the grid body, which exists to pick up DOM changes
* in cells and remove interactive elements from the page's tab index, as
* we want to move between cells via arrow keys instead of tabbing.
*/
export var preventTabbing = function preventTabbing(records) {
// multiple mutation records can implicate the same cell
// so be sure to only check each cell once
var processedCells = new Set();
for (var i = 0; i < records.length; i++) {
var record = records[i];
// find the cell content owning this mutation
var cell = getParentCellContent(record.target);
if (processedCells.has(cell)) continue;
processedCells.add(cell);
if (cell) {
// if we found it, disable tabbable elements
var tabbables = tabbable(cell);
for (var _i = 0; _i < tabbables.length; _i++) {
var element = tabbables[_i];
if (!element.hasAttribute('data-euigrid-tab-managed')) {
element.setAttribute('tabIndex', '-1');
element.setAttribute('data-datagrid-interactable', 'true');
}
}
}
}
};
// Starts with a Node or HTMLElement returned by a mutation record
// and search its ancestors for a div[data-datagrid-cellcontent], if any,
// which is a valid target for disabling tabbing within
export var getParentCellContent = function getParentCellContent(_element) {
var element = _element.nodeType === document.ELEMENT_NODE ? _element : _element.parentElement;
while (element &&
// we haven't walked off the document yet
element.nodeName !== 'div' &&
// looking for a div
!element.hasAttribute('data-datagrid-cellcontent') // that has data-datagrid-cellcontent
) {
element = element.parentElement;
}
return element;
};