Фокус и табуляция

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


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

В стандартных HTML-структурах фокус на элемент может быть установлен через атрибут tabindex. Packery не изменяет поведение фокуса сам по себе, но при динамическом перемещении элементов внутри контейнера важно учитывать, что:

  • Смена порядка DOM-элементов влияет на последовательность табуляции.
  • При перестановке элементов Packery визуальное расположение может не совпадать с порядком в DOM.
  • Для сохранения логичной навигации с клавиатуры нужно явно контролировать tabindex.

Пример установки tabindex

<div class="grid">
  <div class="grid-item" tabindex="0">Элемент 1</div>
  <div class="grid-item" tabindex="0">Элемент 2</div>
  <div class="grid-item" tabindex="0">Элемент 3</div>
</div>

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

const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach((item, index) => {
  item.setAttribute('tabindex', index + 1);
});

Работа с клавишей Tab

Packery управляет позиционированием элементов, но стандартный Tab-переход следует естественному порядку DOM. При интерактивных интерфейсах с возможностью перетаскивания важно:

  1. Сохранять читаемый порядок в DOM — это гарантирует, что Tab перемещает фокус в логической последовательности.
  2. Использовать событие keydown для контроля фокуса, если визуальное расположение элементов отличается от их порядка в DOM.

Пример переопределения поведения Tab для сетки Packery:

document.querySelector('.grid').addEventListener('keydown', (event) => {
  if (event.key === 'Tab') {
    const items = Array.from(document.querySelectorAll('.grid-item'));
    const currentIndex = items.indexOf(document.activeElement);
    let nextIndex = event.shiftKey ? currentIndex - 1 : currentIndex + 1;

    if (nextIndex >= items.length) nextIndex = 0;
    if (nextIndex < 0) nextIndex = items.length - 1;

    items[nextIndex].focus();
    event.preventDefault();
  }
});

Такой подход обеспечивает логическую циклическую навигацию, вне зависимости от того, как Packery разместил элементы визуально.


Сочетание с Drag & Drop

Packery часто используется вместе с библиотеками для перетаскивания элементов, например, Draggabilly. При этом важно учитывать:

  • После завершения перетаскивания фокус может потеряться.
  • Можно явно возвращать фокус на перемещённый элемент:
const pckry = new Packery('.grid', {
  itemSelector: '.grid-item',
  gutter: 10
});

pckry.getItemElements().forEach(itemElem => {
  const draggie = new Draggabilly(itemElem);
  draggie.on('dragEnd', () => {
    itemElem.focus();
  });
});

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


Настройка доступности

Для полной доступности сетки необходимо:

  1. Добавить role="grid" и role="gridcell" к контейнеру и элементам соответственно.
  2. Использовать aria-label или aria-labelledby для элементов, чтобы сообщать их содержимое экранным читалкам.
  3. Обрабатывать клавиши стрелок для перемещения фокуса внутри сетки в дополнение к Tab:
document.querySelector('.grid').addEventListener('keydown', (event) => {
  const items = Array.from(document.querySelectorAll('.grid-item'));
  const currentIndex = items.indexOf(document.activeElement);

  if (event.key === 'ArrowRight') {
    const nextIndex = (currentIndex + 1) % items.length;
    items[nextIndex].focus();
    event.preventDefault();
  }

  if (event.key === 'ArrowLeft') {
    const prevIndex = (currentIndex - 1 + items.length) % items.length;
    items[prevIndex].focus();
    event.preventDefault();
  }
});

Такое управление обеспечивает естественное перемещение по сетке, соответствующее визуальному расположению элементов.


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

Packery поддерживает динамическое добавление и удаление элементов через методы appended и remove. Для сохранения логики табуляции:

const newItem = document.createElement('div');
newItem.className = 'grid-item';
newItem.setAttribute('tabindex', 0);
newItem.textContent = 'Новый элемент';
grid.appendChild(newItem);
pckry.appended(newItem);

// Пересчёт tabindex для всей сетки
const gridItems = document.querySelectorAll('.grid-item');
gridItems.forEach((item, index) => item.setAttribute('tabindex', index + 1));

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


Оптимизация для больших сеток

В больших сетках стоит:

  • Использовать tabindex="-1" для невидимых элементов, чтобы они не попадали в последовательность Tab.
  • Динамически пересчитывать tabindex только при изменении DOM, а не при каждом перестроении Packery.
  • Фокусировать только интерактивные элементы, например кнопки или ссылки внутри grid-item, а не весь контейнер.

Эти методы позволяют Packery корректно работать с фокусом и табуляцией, обеспечивая доступность, предсказуемую навигацию и контроль над взаимодействием клавиатуры с сеткой.