Табличные структуры используются для представления упорядоченных наборов данных. В интерфейсах веб-приложений таблицы часто становятся интерактивными: строки можно выделять, сортировать, редактировать или раскрывать. При этом стандартная клавиатурная навигация браузера ограничена переходом по элементам через клавишу Tab, что не соответствует логике перемещения внутри таблицы.
В интерактивных таблицах ожидается перемещение между ячейками с помощью стрелок, аналогично поведению электронных таблиц. Реализация подобной логики требует:
Библиотека ally.js предоставляет инструменты для работы с фокусом, навигацией и доступностью. При помощи её модулей можно реализовать корректное перемещение между ячейками таблицы, сохраняя соответствие требованиям доступности.
HTML-таблица состоит из элементов:
<table><thead>, <tbody>,
<tfoot><tr><th><td>По умолчанию большинство ячеек не являются фокусируемыми элементами. Даже если внутри ячейки расположен интерактивный элемент (кнопка или ссылка), навигация стрелками не управляет фокусом.
Основные трудности:
1. Отсутствие фокуса на ячейках
Ячейки <td> не получают фокус без явного указания
tabindex.
2. Непредсказуемый порядок Tab-навигации
Если в таблице находятся различные интерактивные элементы, порядок перехода может не совпадать с визуальной сеткой.
3. Сложность поиска соседних элементов
Для перемещения стрелками требуется определить:
4. Игнорирование скрытых элементов
Некоторые ячейки могут быть скрыты стилями или атрибутами доступности.
Для реализации перемещения между ячейками используются несколько частей библиотеки.
Модуль:
ally.is.focusable
Позволяет определить, может ли элемент принимать фокус.
ally.is.focusable(element);
Это важно при работе с ячейками, содержащими интерактивные элементы.
Модуль:
ally.element.focus
Гарантирует корректную установку фокуса.
ally.element.focus(element);
Он учитывает особенности браузеров и предотвращает ошибки при попытке сфокусировать недоступный элемент.
Модуль:
ally.query.focusable
Позволяет получить список всех фокусируемых элементов внутри контейнера.
const focusable = ally.query.focusable({
context: table
});
Чтобы ячейки могли участвовать в перемещении фокуса, необходимо сделать их фокусируемыми.
<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>
Особенности:
Такая схема предотвращает попадание всех ячеек в цепочку 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);
});
Перемещение должно:
tabindexfunction 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;
Интерактивные таблицы часто используют роль:
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.cellsquerySelectorconst 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.
Основные требования:
Использование возможностей ally.js значительно упрощает реализацию этих требований и обеспечивает единообразное управление фокусом в сложных интерфейсах.