Навигация по ячейкам таблицы

Табличные структуры используются для представления упорядоченных наборов данных. В интерфейсах веб-приложений таблицы часто становятся интерактивными: строки можно выделять, сортировать, редактировать или раскрывать. При этом стандартная клавиатурная навигация браузера ограничена переходом по элементам через клавишу Tab, что не соответствует логике перемещения внутри таблицы.

В интерактивных таблицах ожидается перемещение между ячейками с помощью стрелок, аналогично поведению электронных таблиц. Реализация подобной логики требует:

  • определения координат текущей ячейки;
  • вычисления соседних ячеек;
  • управления фокусом;
  • учёта скрытых или недоступных элементов.

Библиотека ally.js предоставляет инструменты для работы с фокусом, навигацией и доступностью. При помощи её модулей можно реализовать корректное перемещение между ячейками таблицы, сохраняя соответствие требованиям доступности.


Проблемы стандартной навигации

HTML-таблица состоит из элементов:

  • <table>
  • <thead>, <tbody>, <tfoot>
  • <tr>
  • <th>
  • <td>

По умолчанию большинство ячеек не являются фокусируемыми элементами. Даже если внутри ячейки расположен интерактивный элемент (кнопка или ссылка), навигация стрелками не управляет фокусом.

Основные трудности:

1. Отсутствие фокуса на ячейках

Ячейки <td> не получают фокус без явного указания tabindex.

2. Непредсказуемый порядок Tab-навигации

Если в таблице находятся различные интерактивные элементы, порядок перехода может не совпадать с визуальной сеткой.

3. Сложность поиска соседних элементов

Для перемещения стрелками требуется определить:

  • текущую строку;
  • текущий столбец;
  • доступность соседней ячейки.

4. Игнорирование скрытых элементов

Некоторые ячейки могут быть скрыты стилями или атрибутами доступности.


Модули ally.js, используемые для навигации

Для реализации перемещения между ячейками используются несколько частей библиотеки.

Определение фокусируемых элементов

Модуль:

ally.is.focusable

Позволяет определить, может ли элемент принимать фокус.

ally.is.focusable(element);

Это важно при работе с ячейками, содержащими интерактивные элементы.


Управление фокусом

Модуль:

ally.element.focus

Гарантирует корректную установку фокуса.

ally.element.focus(element);

Он учитывает особенности браузеров и предотвращает ошибки при попытке сфокусировать недоступный элемент.


Поиск фокусируемых элементов

Модуль:

ally.query.focusable

Позволяет получить список всех фокусируемых элементов внутри контейнера.

const focusable = ally.query.focusable({
  context: table
});

Подготовка таблицы к навигации

Чтобы ячейки могли участвовать в перемещении фокуса, необходимо сделать их фокусируемыми.

Добавление tabindex

<table id="data-table">
  <tr>
    <td tabindex="0">A1</td>
    <td tabindex="-1">A2</td>
    <td tabindex="-1">A3</td>
  </tr>
  <tr>
    <td tabindex="-1">B1</td>
    <td tabindex="-1">B2</td>
    <td tabindex="-1">B3</td>
  </tr>
</table>

Особенности:

  • tabindex=“0” — начальная точка фокуса
  • tabindex=“-1” — элемент может получать фокус программно

Такая схема предотвращает попадание всех ячеек в цепочку Tab-навигации.


Определение координат ячейки

Для перемещения стрелками необходимо вычислить позицию текущей ячейки.

function getCellPosition(cell) {
  const row = cell.parentElement;
  const table = row.parentElement;

  const rowIndex = Array.from(table.children).indexOf(row);
  const colIndex = Array.from(row.children).indexOf(cell);

  return { rowIndex, colIndex };
}

Полученные координаты используются для поиска соседних элементов.


Получение соседней ячейки

Навигация стрелками соответствует следующим направлениям:

Клавиша Направление
ArrowRight следующий столбец
ArrowLeft предыдущий столбец
ArrowDown следующая строка
ArrowUp предыдущая строка

Функция поиска:

function getCell(table, rowIndex, colIndex) {
  const row = table.rows[rowIndex];
  if (!row) return null;

  return row.cells[colIndex] || null;
}

Обработка клавиатурных событий

Событие keydown используется для перехвата нажатий стрелок.

const table = document.getElementById('data-table');

table.addEventListener('keydown', function(event) {
  const cell = event.target.closest('td');
  if (!cell) return;

  const { rowIndex, colIndex } = getCellPosition(cell);

  let nextCell;

  switch (event.key) {
    case 'ArrowRight':
      nextCell = getCell(table, rowIndex, colIndex + 1);
      break;

    case 'ArrowLeft':
      nextCell = getCell(table, rowIndex, colIndex - 1);
      break;

    case 'ArrowDown':
      nextCell = getCell(table, rowIndex + 1, colIndex);
      break;

    case 'ArrowUp':
      nextCell = getCell(table, rowIndex - 1, colIndex);
      break;
  }

  if (!nextCell) return;

  event.preventDefault();

  moveFocus(cell, nextCell);
});

Перемещение фокуса с использованием ally.js

Перемещение должно:

  1. обновлять tabindex
  2. устанавливать фокус через ally.js
function moveFocus(current, next) {
  current.setAttribute('tabindex', '-1');
  next.setAttribute('tabindex', '0');

  ally.element.focus(next);
}

Это обеспечивает корректную работу во всех поддерживаемых браузерах.


Работа с интерактивными элементами внутри ячеек

В реальных интерфейсах ячейки часто содержат:

  • кнопки
  • ссылки
  • поля ввода
  • переключатели

Если фокус перемещается на саму ячейку, внутренние элементы могут оказаться недоступными.

Решение — перенаправлять фокус на первый фокусируемый элемент внутри ячейки.

function focusCell(cell) {
  const focusable = ally.query.focusable({
    context: cell
  });

  if (focusable.length) {
    ally.element.focus(focusable[0]);
  } else {
    ally.element.focus(cell);
  }
}

Навигация с пропуском недоступных ячеек

Некоторые элементы могут быть:

  • скрыты (display: none)
  • отключены (disabled)
  • недоступны для фокуса

Проверка:

function isValidCell(cell) {
  return cell && ally.is.focusable(cell);
}

Использование:

if (nextCell && isValidCell(nextCell)) {
  moveFocus(cell, nextCell);
}

Поддержка динамических таблиц

Во многих интерфейсах строки таблицы добавляются и удаляются динамически.

Особенности:

  • изменение количества строк
  • фильтрация данных
  • виртуализация таблицы

В таких случаях нельзя хранить фиксированные координаты. Каждое перемещение должно вычисляться заново через DOM.


Циклическая навигация

Иногда требуется переход:

  • из последнего столбца строки к первому столбцу следующей строки
  • из первой ячейки к последней

Пример обработки:

case 'ArrowRight':
  nextCell = getCell(table, rowIndex, colIndex + 1);

  if (!nextCell) {
    nextCell = getCell(table, rowIndex + 1, 0);
  }
  break;

Поддержка ARIA-ролей

Интерактивные таблицы часто используют роль:

role="grid"

или

role="table"

Для ячеек:

role="gridcell"

Пример структуры:

<table role="grid">
  <tr role="row">
    <td role="gridcell" tabindex="0">A1</td>
    <td role="gridcell" tabindex="-1">A2</td>
  </tr>
</table>

Это помогает вспомогательным технологиям правильно интерпретировать интерфейс.


Управление фокусом при редактировании

Если внутри ячейки появляется поле ввода, стрелки должны временно отключать навигацию.

Пример проверки:

if (event.target.tagName === 'INPUT') {
  return;
}

Альтернативный подход — включать режим редактирования по клавише Enter.


Оптимизация производительности

В больших таблицах возможны тысячи ячеек. Постоянный поиск соседних элементов может приводить к задержкам.

Оптимизации:

  • использование table.rows и row.cells
  • отказ от глобальных querySelector
  • делегирование событий на уровне таблицы

Пример полной реализации

const table = document.querySelector('#data-table');

table.addEventListener('keydown', function(event) {

  const cell = event.target.closest('td');
  if (!cell) return;

  const row = cell.parentElement;
  const rowIndex = row.rowIndex;
  const colIndex = cell.cellIndex;

  let next;

  switch(event.key) {

    case 'ArrowRight':
      next = table.rows[rowIndex].cells[colIndex + 1];
      break;

    case 'ArrowLeft':
      next = table.rows[rowIndex].cells[colIndex - 1];
      break;

    case 'ArrowDown':
      if (table.rows[rowIndex + 1]) {
        next = table.rows[rowIndex + 1].cells[colIndex];
      }
      break;

    case 'ArrowUp':
      if (table.rows[rowIndex - 1]) {
        next = table.rows[rowIndex - 1].cells[colIndex];
      }
      break;
  }

  if (!next) return;

  event.preventDefault();

  cell.setAttribute('tabindex', '-1');
  next.setAttribute('tabindex', '0');

  ally.element.focus(next);
});

Типичные ошибки реализации

1. Использование только element.focus()

Без ally.js возможны проблемы совместимости браузеров.

2. Неправильный tabindex

Если у всех ячеек tabindex="0", навигация Tab становится неконтролируемой.

3. Отсутствие предотвращения стандартного поведения

Без event.preventDefault() браузер может прокручивать страницу стрелками.

4. Игнорирование вложенных интерактивных элементов

Это нарушает доступность интерфейса.


Рекомендации по доступности

Корректная навигация по таблице должна соответствовать рекомендациям WAI-ARIA Authoring Practices.

Основные требования:

  • стрелки перемещают фокус между ячейками
  • Tab выходит из таблицы
  • Home/End переходят к началу и концу строки
  • PageUp/PageDown перемещают между страницами данных
  • фокус всегда видим

Использование возможностей ally.js значительно упрощает реализацию этих требований и обеспечивает единообразное управление фокусом в сложных интерфейсах.