UNPKG

@elastic/eui

Version:

Elastic UI Component Library

248 lines (242 loc) 10.3 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.useFocus = exports.preventTabbing = exports.notifyCellOfFocusState = exports.getParentCellContent = exports.createKeyDownHandler = exports.DataGridFocusContext = void 0; var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = require("react"); var _tabbable = require("tabbable"); var _services = require("../../../services"); /* * 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. */ var DataGridFocusContext = exports.DataGridFocusContext = /*#__PURE__*/(0, _react.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 */ var useFocus = exports.useFocus = function useFocus() { // Maintain a map of focus cell state callbacks var cellsUpdateFocus = (0, _react.useRef)(new Map()); var onFocusUpdate = (0, _react.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 = (0, _react.useState)(false), _useState2 = (0, _slicedToArray2.default)(_useState, 2), isFocusedCellInView = _useState2[0], setIsFocusedCellInView = _useState2[1]; var _useState3 = (0, _react.useState)(undefined), _useState4 = (0, _slicedToArray2.default)(_useState3, 2), focusedCell = _useState4[0], _setFocusedCell = _useState4[1]; var setFocusedCell = (0, _react.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 = (0, _react.useRef)(undefined); (0, _react.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 = (0, _react.useCallback)(function () { setFocusedCell([0, -1]); }, [setFocusedCell]); var focusProps = (0, _react.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 === _services.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 (0, _react.useMemo)(function () { return { onFocusUpdate: onFocusUpdate, focusedCell: focusedCell, setFocusedCell: setFocusedCell, setIsFocusedCellInView: setIsFocusedCellInView, focusFirstVisibleInteractiveCell: focusFirstVisibleInteractiveCell, focusProps: focusProps }; }, [onFocusUpdate, focusedCell, setFocusedCell, setIsFocusedCellInView, focusFirstVisibleInteractiveCell, focusProps]); }; var notifyCellOfFocusState = exports.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 */ var createKeyDownHandler = exports.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 = (0, _slicedToArray2.default)(focusedCell, 2), x = _focusedCell[0], y = _focusedCell[1]; var key = event.key, ctrlKey = event.ctrlKey; if (key === _services.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 === _services.keys.ARROW_LEFT) { event.preventDefault(); if (x > 0) { setFocusedCell([x - 1, y]); } } else if (key === _services.keys.ARROW_UP) { event.preventDefault(); if (y > -1) { setFocusedCell([x, y - 1]); } } else if (key === _services.keys.ARROW_RIGHT) { event.preventDefault(); if (x < visibleColCount - 1) { setFocusedCell([x + 1, y]); } } else if (key === _services.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 === _services.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 && _services.keys.END)) { event.preventDefault(); setFocusedCell([visibleColCount - 1, visibleRowCount - 1]); } else if (key === (ctrlKey && _services.keys.HOME)) { event.preventDefault(); setFocusedCell([0, 0]); } else if (key === _services.keys.END) { event.preventDefault(); setFocusedCell([visibleColCount - 1, y]); } else if (key === _services.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. */ var preventTabbing = exports.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 = (0, _tabbable.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 var getParentCellContent = exports.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; };