UNPKG

@elastic/eui

Version:

Elastic UI Component Library

273 lines (266 loc) 12.3 kB
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _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(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } /* * 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, useContext, useCallback, useEffect, 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(_ref) { var headerIsInteractive = _ref.headerIsInteractive, gridItemsRendered = _ref.gridItemsRendered; // 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) { // 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 (nextFocusedCell[0] === (focusedCell === null || focusedCell === void 0 ? void 0 : focusedCell[0]) && nextFocusedCell[1] === (focusedCell === null || focusedCell === void 0 ? void 0 : focusedCell[1])) { return; } _setFocusedCell(nextFocusedCell); setIsFocusedCellInView(true); // scrolling.ts ensures focused cells are fully in view }, [focusedCell]); var previousCell = useRef(undefined); useEffect(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 () { if (headerIsInteractive) { // The header (rowIndex -1) is sticky and will always be in view setFocusedCell([0, -1]); } else if (gridItemsRendered.current) { var _gridItemsRendered$cu = gridItemsRendered.current, visibleColumnStartIndex = _gridItemsRendered$cu.visibleColumnStartIndex, visibleRowStartIndex = _gridItemsRendered$cu.visibleRowStartIndex; setFocusedCell([visibleColumnStartIndex, visibleRowStartIndex]); } else { // If the header is non-interactive and there are no rendered cells, // there's nothing to do - we might as well leave focus on the grid body wrapper } }, [setFocusedCell, headerIsInteractive, gridItemsRendered]); 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 { onFocusUpdate: onFocusUpdate, focusedCell: focusedCell, setFocusedCell: setFocusedCell, setIsFocusedCellInView: setIsFocusedCellInView, focusFirstVisibleInteractiveCell: focusFirstVisibleInteractiveCell, focusProps: 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(_ref2) { var gridElement = _ref2.gridElement, visibleColCount = _ref2.visibleColCount, visibleRowCount = _ref2.visibleRowCount, visibleRowStartIndex = _ref2.visibleRowStartIndex, rowCount = _ref2.rowCount, pagination = _ref2.pagination, hasFooter = _ref2.hasFooter, headerIsInteractive = _ref2.headerIsInteractive, focusContext = _ref2.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(); var minimumIndex = headerIsInteractive ? -1 : 0; if (y > minimumIndex) { 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 _i2 = 0; _i2 < tabbables.length; _i2++) { var element = tabbables[_i2]; 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; }; /** * Focus fixes & workarounds */ // If the focus is on the header, and the header is no longer interactive, // move the focus down to the first row export var useHeaderFocusWorkaround = function useHeaderFocusWorkaround(headerIsInteractive) { var _useContext = useContext(DataGridFocusContext), focusedCell = _useContext.focusedCell, setFocusedCell = _useContext.setFocusedCell; useEffect(function () { if (!headerIsInteractive && focusedCell && focusedCell[1] === -1) { setFocusedCell([focusedCell[0], 0]); } }, [headerIsInteractive, focusedCell, setFocusedCell]); };