UNPKG

@neo4j-ndl/react

Version:

React implementation of Neo4j Design System

524 lines 21.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DataGridNav = void 0; /** * * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ const helpers_1 = require("./helpers"); const keys_1 = require("./keys"); const selectors_1 = require("./selectors"); class DataGridNav { constructor(config = {}) { this.keys = []; this.debugLog = (functionName, message) => { if (this.debug) { console.info(`[${functionName}]: ${message}`); } }; const { selectors = {}, pageUpDown, isDebug = false } = config; this.selectors = Object.assign(Object.assign({}, selectors_1.Selectors), selectors); this.pageUpDown = pageUpDown; this.keys = []; this.debug = isDebug; this.disabled = false; } /** * Disables the keyboard listener in cases * that elements inside the grid need to use * arrows keys etc., like select dropdowns */ disable() { this.disabled = true; } /** * Enables the keyboard listeners */ enable() { this.disabled = false; } isFocusable(el) { return el instanceof HTMLElement || el instanceof SVGElement; } focusParentCell(el) { const cell = el.closest(this.selectors.Cell); if (cell && this.isFocusable(cell)) { cell.focus(); } } /** Used as a keyboard listener for key up */ tableKeyUp() { // TODO: have a cleanup as user can press key // and then move to another tab, and get back to the same tab // so this will not be empty (the bug exists with .pop) this.keys = []; } /** Used as a keyboard listener for key down */ tableKeyDown(e) { var _a; this.debugLog('tableKeyDown', `Key pressed: ${e.key}`); if (this.disabled) { this.debugLog('tableKeyDown', 'interaction is disabled'); return; } /** * Avoid page scrolling etc. * Enable default behavior for: * Tab, Shift + Tab * TODO: Actually it will be better to just prevent * in ArrowKeys and PageKeys maybe? * Or should the consumer stop propagation?! * Cannot work with preventDefault as it will * first capture the event from an input in cell for example */ if (keys_1.Keys.ArrowDown === e.key || keys_1.Keys.ArrowUp === e.key || keys_1.Keys.ArrowLeft === e.key || keys_1.Keys.ArrowRight === e.key) { e.preventDefault(); } /** * Add key to the stack if it's * not the same with the last (long press) */ if (this.keys.length === 0 || this.keys[this.keys.length - 1] !== e.key) { this.keys.push(e.key); } /** * Need to check if we are inside a grid cell * or not to enable/disable Grid Navigation */ if (!(e.target instanceof Element)) { return; } const cell = (_a = e.target.parentElement) === null || _a === void 0 ? void 0 : _a.closest(`${this.selectors.Cell},${this.selectors.Row}`); if (!cell) { this.debugLog('tableKeyDown', 'cell not found'); return; } if (cell.matches(this.selectors.Cell)) { this.debugLog('tableKeyDown', 'event captured in cell'); this.cellNavigation(e); } else { this.debugLog('tableKeyDown', 'event captured in grid'); this.gridNavigation(e); } } /** * Handles the navigation inside a cell */ cellNavigation(e) { if (!(e.target instanceof Element)) { return; } const cell = e.target.closest(this.selectors.Cell); if (!cell) { this.debugLog('cellNavigation', 'cell not found'); return; } if (this.getValidFocusableChild(cell)) { this.debugLog('cellNavigation', 'valid focusable child found'); this.gridNavigation(e); return; } const focusableWidgets = cell ? [...cell.querySelectorAll(this.selectors.Focusable)] : []; const widgetIdx = focusableWidgets.findIndex((el) => el === e.target); /** * Keys: Escape * Restore grid navigation */ if (e.key === keys_1.Keys.Escape && this.isFocusable(cell)) { cell.focus(); e.preventDefault(); e.stopPropagation(); return; } /** * Keys: ArrowRight, ArrowDown * Move to the next focusable cell, or the first one * * Arrow Down disabled: * https://github.com/w3c/aria-practices/issues/2739#issuecomment-1613538972 */ if (e.key === keys_1.Keys.ArrowRight || e.key === keys_1.Keys.ArrowDown) { const nextFocusable = widgetIdx === focusableWidgets.length - 1 ? 0 : widgetIdx + 1; const widgetToFocus = focusableWidgets[nextFocusable]; if (this.isFocusable(widgetToFocus)) { widgetToFocus.focus(); } return; } /** * Keys: ArrowLeft, ArrowUp * Move to the previous focusable cell, or the last one * * Arrow Up disabled: * https://github.com/w3c/aria-practices/issues/2739#issuecomment-1613538972 */ if (e.key === keys_1.Keys.ArrowLeft || e.key === keys_1.Keys.ArrowUp) { const previousFocusable = widgetIdx === 0 ? focusableWidgets.length - 1 : widgetIdx - 1; const widgetToFocus = focusableWidgets[previousFocusable]; if (this.isFocusable(widgetToFocus)) { widgetToFocus.focus(); } return; } } /** * Get the valid focusable child of a cell * @return {FocusableElement | null} - The valid focusable child, or null if there are none or multiple focusable children */ getValidFocusableChild(el) { const focusableChildren = [ ...el.querySelectorAll(this.selectors.Focusable), ]; if (focusableChildren.length === 1 && (0, helpers_1.isValidInteractiveElement)(focusableChildren[0]) && this.isFocusable(focusableChildren[0])) { return focusableChildren[0]; } return null; } /** * Handles the navigation outside a cell * on the grid level */ gridNavigation(e) { var _a; if (!(e.target instanceof Element)) { return; } if (this.keys.length === 1) { /** * Keys: Enter * Should move focus inside the cell to the first focusable element: * https://www.w3.org/WAI/ARIA/apg/patterns/grid/#gridNav_inside */ if (e.key === keys_1.Keys.Enter) { const cell = e.target.querySelector(this.selectors.Focusable); if (cell && this.isFocusable(cell)) { // Enter can trigger child elements: // Source: https://www.reddit.com/r/learnjavascript/comments/14kpj24/wrong_keydown_listener_is_called_with_focus/ // If the e.preventDefault causes issues, we can offset the execution with setTimeout cell.focus(); e.preventDefault(); } } /** * Keys: ArrowLeft, ArrowRight * Should move focus to the next/previous cell */ if (e.key === keys_1.Keys.ArrowLeft || e.key === keys_1.Keys.ArrowRight) { const direction = e.key === keys_1.Keys.ArrowLeft ? 'prev' : 'next'; // Get the closest cell we are currently in const cell = e.target.closest(this.selectors.Cell); if (cell && cell instanceof Element) { const closeFocusable = this.findUntil(direction, cell, this.selectors.Cell); if (closeFocusable) { const focusableChild = this.getValidFocusableChild(closeFocusable); const toFocus = focusableChild !== null && focusableChild !== void 0 ? focusableChild : closeFocusable; toFocus.focus(); toFocus.scrollIntoView({ block: 'nearest', inline: 'nearest', }); } } } /** * Keys: Up,Down * Should move focus to the same column of the next/previous row */ if (e.key === keys_1.Keys.ArrowDown || e.key === keys_1.Keys.ArrowUp) { this.verticalCellNavigation(e); return; } /** * Keys: PageUp, PageDown * Should move focus to the first/last row * or a predefined number of rows if user provides a value */ if (e.key === keys_1.Keys.PageUp || e.key === keys_1.Keys.PageDown) { this.pageCellNavigation(e); return; } /** * Keys: Home, End * Should move focus to the first/last cell of the current row */ if (e.key === keys_1.Keys.Home || e.key === keys_1.Keys.End) { const row = e.target.closest(this.selectors.Row); const rowChildren = [...((row === null || row === void 0 ? void 0 : row.children) || [])]; if (e.key === 'End') { rowChildren.reverse(); } this.focusOnFirstCell(rowChildren); } } else { /** * Keys: Control + Home, Control + End * Should move focus to the first/last cell of the first/last row */ const [firstKey, secondKey] = this.keys; if (firstKey === 'Control' && (secondKey === keys_1.Keys.Home || secondKey === keys_1.Keys.End)) { const row = e.target.closest(this.selectors.Row); const siblings = (_a = row.parentElement) === null || _a === void 0 ? void 0 : _a.children; if (!siblings) { this.debugLog('cellNavigation', 'siblings not found'); return; } const rowToFocus = secondKey === keys_1.Keys.Home ? siblings[0] : siblings[siblings.length - 1]; const rowChildren = [...((rowToFocus === null || rowToFocus === void 0 ? void 0 : rowToFocus.children) || [])]; if (secondKey === keys_1.Keys.End) { rowChildren.reverse(); } this.focusOnFirstCell(rowChildren); } } } pageCellNavigation(e) { var _a; if (!(e.target instanceof Element)) { return; } const row = e.target.closest(this.selectors.Row); const cell = e.target.closest(this.selectors.Cell); if (row && cell) { const position = this.getColumnIndex(cell); if (position === undefined) { this.debugLog('cellNavigation', 'position not found'); return; } const direction = e.key === keys_1.Keys.PageUp ? 'prev' : 'next'; const siblings = (_a = row.parentElement) === null || _a === void 0 ? void 0 : _a.children; if (!siblings) { this.debugLog('cellNavigation', 'siblings not found'); return; } // If pageUpDown is defined, we should move that number of rows, or to the closest possible let destinationRow; if (this.pageUpDown) { const methodClbk = direction === 'prev' ? 'previousSibling' : 'nextSibling'; let sibling = row[methodClbk]; if (sibling === null) { return; } let lastVisitedSibling = sibling; for (let i = 0; i < this.pageUpDown - 1 && sibling; i++) { sibling = sibling[methodClbk]; if (sibling) { lastVisitedSibling = sibling; } } destinationRow = sibling ? sibling : lastVisitedSibling; } else { destinationRow = direction === 'prev' ? siblings[0] : siblings[siblings.length - 1]; } if (!destinationRow || !(destinationRow instanceof Element)) { return; } const child = destinationRow.children[position]; if (child && this.isFocusable(child)) { const focusableGrandChild = this.getValidFocusableChild(child); if (focusableGrandChild) { focusableGrandChild.focus(); } else { child.focus(); } } } } verticalCellNavigation(e) { var _a, _b, _c, _d, _e; if (!(e.target instanceof Element)) { return; } const row = e.target.closest(this.selectors.Row); const cell = e.target.closest(this.selectors.Cell); if (row && cell) { const cellPosition = this.getColumnIndex(cell); const rowPosition = this.getRowIndex(row); this.debugLog('gridNavigation', `Initial row position: ${rowPosition}`); this.debugLog('gridNavigation', `Initial cell position: ${cellPosition}`); if (cellPosition === undefined || rowPosition === undefined) { this.debugLog('verticalCellNavigation', 'row or cell position not found'); return; } const direction = e.key === keys_1.Keys.ArrowUp ? 'prev' : 'next'; /** Find previous rowgroup and focus on the proper cell */ if (rowPosition === 0 && direction === 'prev') { const currentRowGroup = (_a = row.parentElement) === null || _a === void 0 ? void 0 : _a.closest(this.selectors.RowGroup); const siblingRowGroups = [ ...(((_b = currentRowGroup === null || currentRowGroup === void 0 ? void 0 : currentRowGroup.parentElement) === null || _b === void 0 ? void 0 : _b.children) || []), ]; const currentRowGroupIdx = siblingRowGroups.findIndex((el) => el === currentRowGroup); if (currentRowGroupIdx !== 0) { const previousRowGroup = siblingRowGroups[currentRowGroupIdx - 1]; const rows = [ ...previousRowGroup.querySelectorAll(this.selectors.Row), ]; const child = rows[rows.length - 1].children[cellPosition]; if (child && this.isFocusable(child)) { const focusableGrandChild = this.getValidFocusableChild(child); if (focusableGrandChild) { focusableGrandChild.focus(); } else { child.focus(); } } return; } } const siblingRows = [ ...(((_c = row.parentElement) === null || _c === void 0 ? void 0 : _c.querySelectorAll(this.selectors.Row)) || []), ]; /** Find next rowgroup and focus on the proper cell */ if (rowPosition === siblingRows.length - 1 && direction === 'next') { const currentRowGroup = (_d = row.parentElement) === null || _d === void 0 ? void 0 : _d.closest(this.selectors.RowGroup); const siblingRowGroups = [ ...(((_e = currentRowGroup === null || currentRowGroup === void 0 ? void 0 : currentRowGroup.parentElement) === null || _e === void 0 ? void 0 : _e.children) || []), ]; const currentRowGroupIdx = siblingRowGroups.findIndex((el) => el === currentRowGroup); if (currentRowGroupIdx !== siblingRowGroups.length - 1) { const nextRowGroup = siblingRowGroups[currentRowGroupIdx + 1]; const rows = [...nextRowGroup.querySelectorAll(this.selectors.Row)]; const child = rows[0].children[cellPosition]; if (child && this.isFocusable(child)) { const focusableGrandChild = this.getValidFocusableChild(child); if (focusableGrandChild) { focusableGrandChild.focus(); } else { child.focus(); } } return; } return; } /** Navigation in the same rowgroup */ const destinationRow = this.findUntil(direction, row, this.selectors.Row); if (!destinationRow) { return; } const child = destinationRow.children[cellPosition]; if (child && this.isFocusable(child)) { const focusableGrandChild = this.getValidFocusableChild(child); if (focusableGrandChild) { focusableGrandChild.focus(); } else { child.focus(); } } } } /** * Sending a row `Element` and then the first cell will be focused. * * If you want to focus the last cell then the row children can be passed in * reversed order */ focusOnFirstCell(el) { for (let i = 0; i < el.length; i++) { const child = el[i]; if (this.isFocusable(child)) { child.focus(); return; } } } /** * Get the column index of a `cell` based on the first `row` parent. * `cellIndex` could be used, but it's not supported in HTML tables. */ getColumnIndex(cell) { var _a; let position = 0; const siblings = (_a = cell === null || cell === void 0 ? void 0 : cell.parentNode) === null || _a === void 0 ? void 0 : _a.children; if (!siblings) { return undefined; } while (cell !== siblings[position] && siblings[position] !== undefined) { position++; } // Cell position find was not possible, maybe should log here if (siblings[position] === undefined) { this.debugLog('getColumnIndex', 'position finding was not successful'); return undefined; } return position; } /** * Get the row index of a `row` based * on its sibling rows */ getRowIndex(row) { var _a; let position = 0; const siblings = (_a = row === null || row === void 0 ? void 0 : row.parentNode) === null || _a === void 0 ? void 0 : _a.children; if (!siblings) { return undefined; } while (row !== siblings[position] && siblings[position] !== undefined) { position++; } // Cell position find was not possible if (siblings[position] === undefined) { this.debugLog('getRowIndex', 'position finding was not successful'); return undefined; } return position; } /** * Equivalent to prevUntil/nextUntil in jQuery * https://api.jquery.com/prevUntil/ */ findUntil(direction, el, matchSelector, exitSelector) { let element = el; const method = direction === 'next' ? 'nextSibling' : 'previousSibling'; while (element[method]) { const sibling = element[method]; if (!sibling) { return null; } if (exitSelector && sibling instanceof Element && sibling.matches(exitSelector)) { return null; } if (sibling instanceof Element && sibling.matches(matchSelector)) { return sibling; } element = sibling; } return null; } } exports.DataGridNav = DataGridNav; //# sourceMappingURL=data-grid-nav.js.map