Навигация с клавиатуры

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

Фокусировка элементов

Для клавиатурной навигации каждый элемент сетки должен быть фокусируемым. Это достигается добавлением атрибута tabindex="0" к элементам, которые будут интерактивными:

<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>

Фокусировка позволяет пользователю перемещаться между элементами с помощью клавиши Tab. После установки фокуса можно реагировать на нажатия стрелок для перемещения или изменения положения элемента.

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

Для управления сеткой через клавиатуру используется метод addEventListener на каждом элементе. Основные события:

  • keydown — отслеживает нажатие клавиш;
  • focus — отслеживает фокусировку элемента.

Пример обработки стрелок для перемещения элементов:

const grid = document.querySelector('.grid');
const pckry = new Packery(grid, {
  itemSelector: '.grid-item',
  gutter: 10
});

grid.addEventListener('keydown', (event) => {
  const item = event.target;
  if (!item.classList.contains('grid-item')) return;

  let deltaX = 0, deltaY = 0;

  switch(event.key) {
    case 'ArrowUp': deltaY = -1; break;
    case 'ArrowDown': deltaY = 1; break;
    case 'ArrowLeft': deltaX = -1; break;
    case 'ArrowRight': deltaX = 1; break;
    case 'Enter':
      pckry.fit(item, item.offsetLeft, item.offsetTop); 
      break;
  }

  if (deltaX || deltaY) {
    moveItemWithKeyboard(item, deltaX, deltaY);
  }
});

function moveItemWithKeyboard(item, dx, dy) {
  const style = window.getComputedStyle(item);
  const currentLeft = parseInt(style.left, 10) || 0;
  const currentTop = parseInt(style.top, 10) || 0;

  item.style.left = currentLeft + dx * 20 + 'px';
  item.style.top = currentTop + dy * 20 + 'px';

  pckry.layout();
}

Поддержка модификаторов

Для более сложного взаимодействия удобно использовать клавиши-модификаторы (Shift, Ctrl, Alt) вместе со стрелками. Например:

  • Shift + стрелка — перемещение элемента на более крупный шаг;
  • Ctrl + стрелка — групповое выделение элементов;
  • Alt + Enter — включение режима редактирования содержимого элемента.

Пример интеграции модификатора Shift:

const step = 20;
const bigStep = 60;

function moveItemWithKeyboard(item, dx, dy, big = false) {
  const size = big ? bigStep : step;
  const style = window.getComputedStyle(item);
  const currentLeft = parseInt(style.left, 10) || 0;
  const currentTop = parseInt(style.top, 10) || 0;

  item.style.left = currentLeft + dx * size + 'px';
  item.style.top = currentTop + dy * size + 'px';

  pckry.layout();
}

grid.addEventListener('keydown', (event) => {
  const big = event.shiftKey;
  switch(event.key) {
    case 'ArrowUp': moveItemWithKeyboard(event.target, 0, -1, big); break;
    case 'ArrowDown': moveItemWithKeyboard(event.target, 0, 1, big); break;
    case 'ArrowLeft': moveItemWithKeyboard(event.target, -1, 0, big); break;
    case 'ArrowRight': moveItemWithKeyboard(event.target, 1, 0, big); break;
  }
});

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

После изменения позиции элемента важно сохранять фокус на нем, иначе пользователь потеряет ориентацию в сетке. Для этого достаточно вызывать element.focus() после завершения перемещения:

function moveItemWithKeyboard(item, dx, dy, big = false) {
  const size = big ? bigStep : step;
  const style = window.getComputedStyle(item);
  const currentLeft = parseInt(style.left, 10) || 0;
  const currentTop = parseInt(style.top, 10) || 0;

  item.style.left = currentLeft + dx * size + 'px';
  item.style.top = currentTop + dy * size + 'px';

  pckry.layout();
  item.focus();
}

Поддержка вспомогательных библиотек

Packery совместим с Draggabilly и jQuery UI Draggable. Для клавиатурной навигации это позволяет объединять методы:

  • Перемещение через клавиатуру синхронизируется с draggable;
  • Можно активировать клавиатурный режим только после двойного клика или Enter;
  • Поддержка dragEnd позволяет обновлять сетку после перемещения клавишами.
const draggies = [];
pckry.getItemElements().forEach(function(itemElem) {
  const draggie = new Draggabilly(itemElem);
  draggies.push(draggie);

  itemElem.addEventListener('keydown', (event) => {
    if (event.key === 'Enter') draggie.enable();
    if (event.key === 'Escape') draggie.disable();
  });
});

Адаптация к доступности

Клавиатурная навигация должна учитывать ARIA-атрибуты:

  • role="grid" для контейнера;
  • role="gridcell" для элементов;
  • aria-selected="true/false" для выделенных элементов;
  • aria-grabbed="true/false" при перемещении.
<div class="grid" role="grid">
  <div class="grid-item" role="gridcell" tabindex="0" aria-selected="false">Элемент 1</div>
</div>

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

Итоги технической реализации

  • Каждый элемент сетки должен быть фокусируемым (tabindex="0").
  • Перемещение осуществляется через события keydown.
  • Стрелки управляют направлением, модификаторы увеличивают шаг или активируют дополнительные функции.
  • После перемещения фокус нужно сохранять.
  • Интеграция с Draggabilly и jQuery UI расширяет возможности перемещения.
  • ARIA-атрибуты делают интерфейс доступным и для вспомогательных технологий.

Такой подход обеспечивает полное управление сеткой Packery с клавиатуры без потери функциональности drag-and-drop и без снижения доступности.