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

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

Клавиатурная навигация позволяет:

  • перемещаться между виджетами сетки без мыши
  • изменять положение элементов
  • изменять размеры блоков
  • активировать взаимодействие с выбранным элементом
  • повышать доступность интерфейса (Accessibility)

GridStack не предоставляет полноценной встроенной системы управления сеткой с клавиатуры, однако архитектура библиотеки позволяет реализовать её на уровне событий DOM и API GridStack.

Основой такой системы являются:

  • управление фокусом
  • обработка клавиатурных событий
  • использование методов GridStack API
  • синхронизация состояния DOM и сетки

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

Каждый элемент сетки в GridStack представлен DOM-узлом с классом:

.grid-stack-item

Чтобы элемент мог получать фокус с клавиатуры, ему необходимо назначить атрибут:

tabindex="0"

Пример виджета:

<div class="grid-stack-item" gs-x="0" gs-y="0" gs-w="2" gs-h="2" tabindex="0">
  <div class="grid-stack-item-content">
    Виджет
  </div>
</div>

Теперь элемент может получать фокус при навигации клавишей Tab.

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

const grid = GridStack.init();

document.querySelectorAll('.grid-stack-item').forEach(el => {
  el.setAttribute('tabindex', '0');
});

Фокусируемый элемент становится отправной точкой для клавиатурных действий.


Отслеживание активного виджета

Для управления выбранным элементом необходимо хранить ссылку на активный widget.

let activeWidget = null;

document.querySelector('.grid-stack').addEventListener('focusin', (event) => {
  const item = event.target.closest('.grid-stack-item');

  if (item) {
    activeWidget = item;
  }
});

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


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

Основной механизм взаимодействия — обработчик keydown.

document.addEventListener('keydown', (event) => {

  if (!activeWidget) return;

  switch (event.key) {

    case 'ArrowLeft':
      moveWidget(-1, 0);
      break;

    case 'ArrowRight':
      moveWidget(1, 0);
      break;

    case 'ArrowUp':
      moveWidget(0, -1);
      break;

    case 'ArrowDown':
      moveWidget(0, 1);
      break;

  }

});

Стрелки используются для перемещения виджета внутри сетки.


Получение координат элемента

GridStack хранит координаты элемента внутри объекта GridStackNode.

Получить его можно через DOM-элемент.

const node = activeWidget.gridstackNode;

Основные параметры:

Параметр Назначение
x позиция по горизонтали
y позиция по вертикали
w ширина
h высота

Перемещение виджета с клавиатуры

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

grid.move()

Функция перемещения:

function moveWidget(dx, dy) {

  const node = activeWidget.gridstackNode;

  const newX = node.x + dx;
  const newY = node.y + dy;

  grid.move(activeWidget, newX, newY);

}

Теперь стрелки клавиатуры перемещают виджет по сетке.


Ограничение перемещения внутри сетки

Без ограничений элемент может выйти за границы сетки. Перед перемещением выполняется проверка.

function moveWidget(dx, dy) {

  const node = activeWidget.gridstackNode;

  let newX = node.x + dx;
  let newY = node.y + dy;

  if (newX < 0) newX = 0;
  if (newY < 0) newY = 0;

  if (newX + node.w > grid.engine.column)
    newX = grid.engine.column - node.w;

  grid.move(activeWidget, newX, newY);

}

Такое ограничение предотвращает выход виджета за пределы сетки.


Изменение размера виджета с клавиатуры

Для управления размерами удобно использовать комбинацию клавиш Shift + стрелка.

Обработчик:

document.addEventListener('keydown', (event) => {

  if (!activeWidget) return;

  if (event.shiftKey) {

    switch (event.key) {

      case 'ArrowRight':
        resizeWidget(1, 0);
        break;

      case 'ArrowLeft':
        resizeWidget(-1, 0);
        break;

      case 'ArrowDown':
        resizeWidget(0, 1);
        break;

      case 'ArrowUp':
        resizeWidget(0, -1);
        break;

    }

  }

});

Функция изменения размеров:

function resizeWidget(dw, dh) {

  const node = activeWidget.gridstackNode;

  let newW = node.w + dw;
  let newH = node.h + dh;

  if (newW < 1) newW = 1;
  if (newH < 1) newH = 1;

  grid.resize(activeWidget, newW, newH);

}

Ограничения размеров

Чтобы избежать некорректных размеров, используются параметры:

  • minW
  • minH
  • maxW
  • maxH

Они могут задаваться в атрибутах:

<div class="grid-stack-item"
     gs-w="2"
     gs-h="2"
     gs-min-w="1"
     gs-max-w="6">
</div>

В функции изменения размера можно учитывать ограничения.

if (node.maxW && newW > node.maxW) newW = node.maxW;
if (node.minW && newW < node.minW) newW = node.minW;

Переключение между виджетами

Стандартная клавиша Tab перемещает фокус по DOM-структуре. Однако порядок может не совпадать с визуальной сеткой.

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

Например, при нажатии Alt + стрелка.

if (event.altKey) {

  switch (event.key) {

    case 'ArrowRight':
      focusNeighbor(1,0);
      break;

    case 'ArrowLeft':
      focusNeighbor(-1,0);
      break;

  }

}

Поиск соседнего элемента:

function focusNeighbor(dx, dy) {

  const node = activeWidget.gridstackNode;

  const targetX = node.x + dx;
  const targetY = node.y + dy;

  const widgets = grid.engine.nodes;

  const neighbor = widgets.find(w =>
    w.x === targetX && w.y === targetY
  );

  if (neighbor && neighbor.el) {
    neighbor.el.focus();
  }

}

Визуальная индикация фокуса

Для удобства навигации активный элемент должен выделяться.

CSS:

.grid-stack-item:focus {
  outline: 3px solid #4a90e2;
}

Можно также добавлять отдельный класс:

document.addEventListener('focusin', (event) => {

  document
    .querySelectorAll('.grid-stack-item')
    .forEach(el => el.classList.remove('active-widget'));

  const item = event.target.closest('.grid-stack-item');

  if (item) {
    item.classList.add('active-widget');
  }

});

CSS:

.active-widget {
  box-shadow: 0 0 0 3px #2196f3;
}

Блокировка перемещения

Иногда требуется режим, при котором клавиатура не перемещает элементы.

GridStack поддерживает блокировку:

grid.disableMove(true);
grid.disableResize(true);

Клавиатурная логика может проверять состояние:

if (grid.opts.disableDrag) return;

Использование режима редактирования

Для интерфейсов панелей управления часто используется режим редактирования.

В обычном режиме:

  • клавиатура перемещает фокус
  • виджеты статичны

В режиме редактирования:

  • стрелки перемещают виджеты
  • Shift + стрелки меняют размер

Пример переключения режима:

let editMode = false;

function toggleEditMode() {
  editMode = !editMode;
}

В обработчике клавиш:

if (!editMode) return;

Удаление виджета с клавиатуры

Удаление может выполняться клавишей Delete.

document.addEventListener('keydown', (event) => {

  if (!activeWidget) return;

  if (event.key === 'Delete') {

    grid.removeWidget(activeWidget);
    activeWidget = null;

  }

});

Добавление нового виджета с клавиатуры

Можно реализовать создание элемента, например клавишей Insert.

document.addEventListener('keydown', (event) => {

  if (event.key === 'Insert') {

    const widget = grid.addWidget({
      w: 2,
      h: 2,
      content: 'Новый виджет'
    });

    widget.setAttribute('tabindex','0');
    widget.focus();

  }

});

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

При активном управлении с клавиатуры важно избегать лишних перерасчётов сетки.

Рекомендуется:

  • использовать grid.batchUpdate()
  • минимизировать DOM-операции
  • не вызывать grid.engine напрямую без необходимости

Пример:

grid.batchUpdate();

grid.move(widget, x, y);

grid.commit();

Поддержка доступности (Accessibility)

Для соответствия стандартам ARIA элементы сетки могут иметь роли.

<div class="grid-stack" role="grid">

Виджет:

<div class="grid-stack-item" role="gridcell">

Активный элемент:

aria-selected="true"

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


Архитектура клавиатурной системы

Полноценная система управления GridStack с клавиатуры обычно включает:

Модуль фокуса

  • отслеживает активный виджет

Модуль навигации

  • перемещение между виджетами

Модуль трансформации

  • перемещение
  • изменение размера

Модуль режимов

  • просмотр
  • редактирование

Такое разделение упрощает поддержку сложных интерфейсов.


Пример базовой реализации

const grid = GridStack.init();

let activeWidget = null;

document.querySelector('.grid-stack')
  .addEventListener('focusin', e => {

    const item = e.target.closest('.grid-stack-item');

    if (item) activeWidget = item;

});

document.addEventListener('keydown', e => {

  if (!activeWidget) return;

  const node = activeWidget.gridstackNode;

  switch (e.key) {

    case 'ArrowLeft':
      grid.move(activeWidget, node.x - 1, node.y);
      break;

    case 'ArrowRight':
      grid.move(activeWidget, node.x + 1, node.y);
      break;

    case 'ArrowUp':
      grid.move(activeWidget, node.x, node.y - 1);
      break;

    case 'ArrowDown':
      grid.move(activeWidget, node.x, node.y + 1);
      break;

  }

});

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