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;
}
}
}
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-атрибуты, чтобы вспомогательные технологии (экранные читалки, клавиатурные навигаторы) корректно интерпретировали элементы интерфейса:
<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, позволяя создавать интерактивные интерфейсы, которые корректно работают с клавиатурой и поддерживают стандарты доступности.