@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
205 lines (204 loc) • 6.17 kB
JavaScript
"use strict";
"use client";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useTableKeyboardNavigation = useTableKeyboardNavigation;
var _react = require("react");
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
function getCells(table) {
const rows = Array.from(table.querySelectorAll('tr')).filter(row => row.closest('table') === table);
return rows.map(row => Array.from(row.querySelectorAll('td, th')).filter(cell => cell.closest('tr') === row));
}
function findCellPosition(grid, target) {
for (let row = 0; row < grid.length; row++) {
for (let col = 0; col < grid[row].length; col++) {
if (grid[row][col] === target || grid[row][col].contains(target)) {
return {
row,
col
};
}
}
}
return null;
}
function focusCell(cell) {
const focusable = cell.querySelector(FOCUSABLE_SELECTOR);
if (focusable) {
focusable.focus();
} else {
if (!cell.hasAttribute('tabindex')) {
cell.setAttribute('tabindex', '-1');
}
cell.classList.add('dnb-no-focus', 'dnb-table__cell--focus');
cell.focus();
}
}
function setupTabindex(table) {
const cells = table.querySelectorAll('td, th');
cells.forEach(cell => {
const hasFocusable = cell.querySelector(FOCUSABLE_SELECTOR);
if (!hasFocusable && !cell.hasAttribute('tabindex')) {
cell.setAttribute('tabindex', '-1');
}
});
}
function setupFocusCleanup(table) {
const handler = event => {
const target = event.target;
if (target.classList.contains('dnb-table__cell--focus')) {
target.classList.remove('dnb-no-focus', 'dnb-table__cell--focus');
}
};
table.addEventListener('focusout', handler);
return () => {
table.removeEventListener('focusout', handler);
};
}
const HORIZONTAL_NAV_SELECTOR = 'input[type="range"], [role="slider"]';
const VERTICAL_NAV_SELECTOR = 'input[type="number"], [role="spinbutton"], select, [role="listbox"]';
function isTextInput(element) {
if (element instanceof HTMLTextAreaElement) {
return true;
}
if (element instanceof HTMLInputElement) {
const textTypes = ['text', 'search', 'url', 'tel', 'email', 'password', 'number'];
return textTypes.includes(element.type);
}
return false;
}
function shouldSkipHorizontalNav(target) {
return target.closest(HORIZONTAL_NAV_SELECTOR) !== null;
}
function shouldSkipVerticalNav(target) {
return target.closest(VERTICAL_NAV_SELECTOR) !== null;
}
function isAtInputBoundary(target, direction) {
const {
selectionStart,
selectionEnd,
value
} = target;
if (selectionStart !== selectionEnd) {
return false;
}
if (direction === 'left') {
return selectionStart === 0;
}
return selectionEnd === value.length;
}
function isTextareaAtVerticalBoundary(textarea, direction) {
const {
selectionStart,
selectionEnd,
value
} = textarea;
if (selectionStart !== selectionEnd) {
return false;
}
if (direction === 'up') {
const textBeforeCursor = value.substring(0, selectionStart);
return !textBeforeCursor.includes('\n');
}
const textAfterCursor = value.substring(selectionStart);
return !textAfterCursor.includes('\n');
}
function handleKeyDown(event, table) {
var _grid$row$length, _grid$row, _grid$position$row, _grid$row2;
const {
key
} = event;
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) {
return;
}
const target = event.target;
const isHorizontal = key === 'ArrowLeft' || key === 'ArrowRight';
if (isHorizontal && shouldSkipHorizontalNav(target)) {
return;
}
if (!isHorizontal && shouldSkipVerticalNav(target)) {
return;
}
if (!isHorizontal && target instanceof HTMLTextAreaElement) {
const direction = key === 'ArrowUp' ? 'up' : 'down';
if (!isTextareaAtVerticalBoundary(target, direction)) {
return;
}
}
if (isHorizontal && isTextInput(target)) {
const direction = key === 'ArrowLeft' ? 'left' : 'right';
if (!isAtInputBoundary(target, direction)) {
return;
}
}
const grid = getCells(table);
const position = findCellPosition(grid, target);
if (!position) {
return;
}
let {
row,
col
} = position;
switch (key) {
case 'ArrowUp':
row = Math.max(0, row - 1);
break;
case 'ArrowDown':
row = Math.min(grid.length - 1, row + 1);
break;
case 'ArrowLeft':
col = Math.max(0, col - 1);
break;
case 'ArrowRight':
col = Math.min(((_grid$row$length = (_grid$row = grid[row]) === null || _grid$row === void 0 ? void 0 : _grid$row.length) !== null && _grid$row$length !== void 0 ? _grid$row$length : 1) - 1, col + 1);
break;
}
const currentCell = (_grid$position$row = grid[position.row]) === null || _grid$position$row === void 0 ? void 0 : _grid$position$row[position.col];
const nextCell = (_grid$row2 = grid[row]) === null || _grid$row2 === void 0 ? void 0 : _grid$row2[col];
if (nextCell && nextCell !== currentCell) {
event.preventDefault();
focusCell(nextCell);
}
}
function useTableKeyboardNavigation({
enabled = true
} = {}) {
const ref = (0, _react.useRef)(null);
const getTable = (0, _react.useCallback)(() => {
const el = ref.current;
if (!el) {
return null;
}
if (el.tagName === 'TABLE') {
return el;
}
return el.querySelector('table');
}, []);
const handler = (0, _react.useCallback)(event => {
const table = getTable();
if (table) {
handleKeyDown(event, table);
}
}, [getTable]);
(0, _react.useEffect)(() => {
const el = ref.current;
if (!el || !enabled) {
return undefined;
}
const table = getTable();
if (!table) {
return undefined;
}
setupTabindex(table);
const cleanupFocus = setupFocusCleanup(table);
el.addEventListener('keydown', handler);
return () => {
el.removeEventListener('keydown', handler);
cleanupFocus();
};
}, [enabled, handler, getTable]);
return ref;
}
//# sourceMappingURL=useTableKeyboardNavigation.js.map