Viewing File: /home/markqprx/iniasli.pro/client/ui/tables/navigate-grid.ts

import React, {KeyboardEventHandler} from 'react';
import {getFocusableTreeWalker} from '@react-aria/focus';
import {focusWithoutScrolling} from '@react-aria/utils';
import {isCtrlKeyPressed} from '../../utils/keybinds/is-ctrl-key-pressed';

interface Props {
  cellCount: number;
  rowCount: number;
}
export function useGridNavigation(props: Props) {
  const {cellCount, rowCount} = props;
  const onKeyDown: KeyboardEventHandler<HTMLElement> = e => {
    switch (e.key) {
      case 'ArrowLeft':
        focusSiblingCell(e, {cell: {op: 'decrement'}}, props);
        break;
      case 'ArrowRight':
        focusSiblingCell(e, {cell: {op: 'increment'}}, props);
        break;
      case 'ArrowUp':
        focusSiblingCell(e, {row: {op: 'decrement'}}, props);
        break;
      case 'ArrowDown':
        focusSiblingCell(e, {row: {op: 'increment'}}, props);
        break;
      case 'PageUp':
        focusSiblingCell(e, {row: {op: 'decrement', count: 5}}, props);
        break;
      case 'PageDown':
        focusSiblingCell(e, {row: {op: 'increment', count: 5}}, props);
        break;
      case 'Tab':
        focusFirstElementAfterGrid(e);
        break;
      case 'Home':
        if (isCtrlKeyPressed(e)) {
          // move to first cell in first row
          focusSiblingCell(
            e,
            {
              row: {op: 'decrement', count: rowCount},
              cell: {op: 'decrement', count: cellCount},
            },
            props
          );
        } else {
          // move to first cell in current row
          focusSiblingCell(
            e,
            {cell: {op: 'decrement', count: cellCount}},
            props
          );
        }
        break;
      case 'End':
        if (isCtrlKeyPressed(e)) {
          // move to last cell in last row
          focusSiblingCell(
            e,
            {
              row: {op: 'increment', count: rowCount},
              cell: {op: 'increment', count: cellCount},
            },
            props
          );
        } else {
          // move to last cell in current row
          focusSiblingCell(
            e,
            {cell: {op: 'increment', count: cellCount}},
            props
          );
        }
        break;
    }
  };

  return {onKeyDown};
}

interface Operations {
  cell?: {
    op: 'increment' | 'decrement';
    count?: number;
  };
  row?: {
    op: 'increment' | 'decrement';
    count?: number;
  };
}
function focusSiblingCell(
  e: React.KeyboardEvent,
  operations: Operations,
  {cellCount, rowCount}: Props
) {
  if (document.activeElement?.tagName === 'input') return;
  e.preventDefault();
  const grid = e.currentTarget as HTMLElement;

  // focused element might be inside the cell and not the cell itself
  const currentCell = (e.target as HTMLElement).closest('[aria-colindex]');
  if (!currentCell || !grid) return;

  const row = currentCell.closest('[aria-rowindex]');
  if (!row) return;

  // grab row and cell index from aria attributes
  let rowIndex = parseInt(row.getAttribute('aria-rowindex') as string);
  let cellIndex = parseInt(currentCell.getAttribute('aria-colindex') as string);
  if (Number.isNaN(rowIndex) || Number.isNaN(cellIndex)) return;

  // adjust row index for next cell selector
  const rowOpCount = operations.row?.count ?? 1;
  if (operations.row?.op === 'increment') {
    rowIndex = Math.min(rowCount, rowIndex + rowOpCount);
  } else if (operations.row?.op === 'decrement') {
    rowIndex = Math.max(1, rowIndex - rowOpCount);
  }

  // adjust cell index for next cell selector
  const cellOpCount = operations.cell?.count ?? 1;
  if (operations.cell?.op === 'increment') {
    cellIndex = Math.min(cellCount, cellIndex + cellOpCount);
  } else if (operations.cell?.op === 'decrement') {
    cellIndex = Math.max(1, cellIndex - cellOpCount);
  }

  // find the next cell that should be focused
  const nextCell = grid.querySelector(
    `[aria-rowindex="${rowIndex}"] [aria-colindex="${cellIndex}"]`
  ) as HTMLElement | undefined;
  if (!nextCell) return;

  // find any focusable elements inside the cell
  const walker = getFocusableTreeWalker(nextCell);
  const nextFocusableElement = (walker.nextNode() || nextCell) as HTMLElement;

  // adjust tab index on current and next cells and focus either next cell or first focusable element inside that cell
  currentCell.setAttribute('tabindex', '-1');
  nextFocusableElement.setAttribute('tabindex', '0');
  nextFocusableElement.focus();
}

// grid is treated as a single tab stop, focus first element after grid, instead of moving focus withing grid on tab press
function focusFirstElementAfterGrid(e: React.KeyboardEvent) {
  const grid = e.currentTarget as HTMLElement;
  if (e.shiftKey) {
    grid.focus();
  } else {
    const walker = getFocusableTreeWalker(grid, {tabbable: true});
    let next: HTMLElement;
    let last: HTMLElement;
    do {
      last = walker.lastChild() as HTMLElement;
      if (last) {
        next = last;
      }
    } while (last);

    // @ts-ignore
    if (next && !next.contains(document.activeElement)) {
      focusWithoutScrolling(next);
    }
  }
}
Back to Directory File Manager