Клавиатурная навигация

Stimulus — это легковесный JavaScript-фреймворк, ориентированный на добавление поведения к HTML через декларативные атрибуты. Одним из ключевых аспектов интерактивного интерфейса является поддержка клавиатурной навигации, которая обеспечивает доступность и удобство управления элементами без использования мыши.

Клавиатурная навигация строится на событиях keyboard events, таких как keydown, keyup и keypress. Stimulus позволяет организовать обработку этих событий с помощью контроллеров, связывая действия с конкретными элементами DOM через атрибут data-action.


Определение контроллера для клавиатурного взаимодействия

Контроллер создается через стандартную структуру Stimulus:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["item"];
  static values = { index: Number }

  connect() {
    this.indexValue = 0;
    this.focusItem(this.indexValue);
  }

  focusItem(index) {
    if (this.hasItemTarget) {
      this.itemTargets.forEach((el, i) => {
        el.tabIndex = i === index ? 0 : -1;
      });
      this.itemTargets[index].focus();
    }
  }

  next() {
    if (this.indexValue < this.itemTargets.length - 1) {
      this.indexValue++;
      this.focusItem(this.indexValue);
    }
  }

  previous() {
    if (this.indexValue > 0) {
      this.indexValue--;
      this.focusItem(this.indexValue);
    }
  }

  keydown(event) {
    switch (event.key) {
      case "ArrowDown":
        event.preventDefault();
        this.next();
        break;
      case "ArrowUp":
        event.preventDefault();
        this.previous();
        break;
      case "Home":
        event.preventDefault();
        this.indexValue = 0;
        this.focusItem(this.indexValue);
        break;
      case "End":
        event.preventDefault();
        this.indexValue = this.itemTargets.length - 1;
        this.focusItem(this.indexValue);
        break;
    }
  }
}

Привязка событий к DOM

Stimulus позволяет декларативно связывать события с методами контроллера через атрибут data-action:

<ul data-controller="keyboard-navigation" data-action="keydown->keyboard-navigation#keydown">
  <li data-keyboard-navigation-target="item">Элемент 1</li>
  <li data-keyboard-navigation-target="item">Элемент 2</li>
  <li data-keyboard-navigation-target="item">Элемент 3</li>
</ul>
  • data-controller="keyboard-navigation" — подключение контроллера к списку.
  • data-keyboard-navigation-target="item" — определение элементов, между которыми будет происходить навигация.
  • data-action="keydown->keyboard-navigation#keydown" — связывает событие keydown с методом keydown контроллера.

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

Для обеспечения доступности важно использовать ARIA-атрибуты, чтобы вспомогательные технологии (экранные читалки, клавиатурные навигаторы) корректно интерпретировали элементы интерфейса:

<ul role="listbox" aria-activedescendant="item-0" data-controller="keyboard-navigation" data-action="keydown->keyboard-navigation#keydown">
  <li id="item-0" role="option" data-keyboard-navigation-target="item">Элемент 1</li>
  <li id="item-1" role="option" data-keyboard-navigation-target="item">Элемент 2</li>
  <li id="item-2" role="option" data-keyboard-navigation-target="item">Элемент 3</li>
</ul>

Метод focusItem необходимо дополнить обновлением aria-activedescendant:

focusItem(index) {
  this.itemTargets.forEach((el, i) => {
    el.tabIndex = i === index ? 0 : -1;
  });
  this.element.setAttribute("aria-activedescendant", this.itemTargets[index].id);
  this.itemTargets[index].focus();
}

Обработка дополнительных клавиш

Помимо стандартных стрелок, рекомендуется обрабатывать клавиши Enter, Space, PageUp и PageDown, чтобы улучшить взаимодействие с элементами:

keydown(event) {
  switch (event.key) {
    case "ArrowDown":
      event.preventDefault();
      this.next();
      break;
    case "ArrowUp":
      event.preventDefault();
      this.previous();
      break;
    case "Home":
      event.preventDefault();
      this.indexValue = 0;
      this.focusItem(this.indexValue);
      break;
    case "End":
      event.preventDefault();
      this.indexValue = this.itemTargets.length - 1;
      this.focusItem(this.indexValue);
      break;
    case "Enter":
    case " ":
      event.preventDefault();
      this.selectItem(this.indexValue);
      break;
  }
}

selectItem(index) {
  const selectedItem = this.itemTargets[index];
  selectedItem.classList.toggle("selected");
}

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


Динамическое обновление элементов

Stimulus упрощает работу с динамическим DOM. Если элементы списка добавляются или удаляются после загрузки страницы, достаточно вызвать методы обновления itemTargets и корректно сбросить индекс:

refreshTargets() {
  this.indexValue = 0;
  this.focusItem(this.indexValue);
}

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


Комбинация с другими контроллерами

Stimulus поддерживает несколько контроллеров на одном элементе. Это позволяет совместить клавиатурную навигацию с, например, контроллерами модальных окон, автодополнения или сортируемых списков. Основное правило — аккуратно управлять event.preventDefault() и stopPropagation(), чтобы события не конфликтовали между контроллерами.


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