UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

200 lines (199 loc) 5.74 kB
"use client"; import { useCallback, useEffect, useRef } from '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; 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]?.length) !== null && _grid$row$length !== void 0 ? _grid$row$length : 1) - 1, col + 1); break; } const currentCell = grid[position.row]?.[position.col]; const nextCell = grid[row]?.[col]; if (nextCell && nextCell !== currentCell) { event.preventDefault(); focusCell(nextCell); } } export function useTableKeyboardNavigation({ enabled = true } = {}) { const ref = useRef(null); const getTable = useCallback(() => { const el = ref.current; if (!el) { return null; } if (el.tagName === 'TABLE') { return el; } return el.querySelector('table'); }, []); const handler = useCallback(event => { const table = getTable(); if (table) { handleKeyDown(event, table); } }, [getTable]); 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