@neo4j-ndl/react
Version:
React implementation of Neo4j Design System
524 lines • 21.3 kB
JavaScript
"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