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

Lit предоставляет удобные инструменты для создания веб-компонентов с управлением состоянием и реактивными свойствами. Одной из важных задач при разработке интерактивных компонентов является поддержка навигации с клавиатуры для обеспечения доступности и удобства использования.

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

В Lit обработка клавиатуры реализуется через стандартные DOM-события: keydown, keyup и keypress. Наиболее часто используется keydown, поскольку оно срабатывает при нажатии клавиши и позволяет предотвращать стандартное поведение браузера.

Пример простого обработчика:

import { LitElement, html, css } from 'lit';

class KeyboardNavExample extends LitElement {
  static styles = css`
    .item {
      padding: 8px;
      cursor: pointer;
    }
    .item:focus {
      outline: 2px solid blue;
    }
  `;

  render() {
    return html`
      <div
        class="item"
        tabindex="0"
        @keydown="${this._handleKeyDown}"
      >
        Элемент для навигации
      </div>
    `;
  }

  _handleKeyDown(event) {
    switch (event.key) {
      case 'ArrowUp':
        console.log('Вверх');
        break;
      case 'ArrowDown':
        console.log('Вниз');
        break;
      case 'Enter':
        console.log('Активирован элемент');
        break;
    }
  }
}

customElements.define('keyboard-nav-example', KeyboardNavExample);

Ключевые моменты:

  • Атрибут tabindex="0" делает элемент доступным для фокусировки клавишей Tab.
  • Обработчик @keydown получает объект события с ключевым свойством key, определяющим нажатую клавишу.
  • Использование switch позволяет легко добавлять обработку различных клавиш.

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

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

this.shadowRoot.querySelectorAll('.item')[index].focus();

В комбинации с обработкой клавиш ArrowUp и ArrowDown можно перемещать фокус по списку элементов:

_handleKeyDown(event) {
  const items = Array.from(this.shadowRoot.querySelectorAll('.item'));
  const currentIndex = items.indexOf(this.shadowRoot.activeElement);
  
  if (event.key === 'ArrowDown' && currentIndex < items.length - 1) {
    items[currentIndex + 1].focus();
    event.preventDefault();
  }

  if (event.key === 'ArrowUp' && currentIndex > 0) {
    items[currentIndex - 1].focus();
    event.preventDefault();
  }
}

Особенности реализации:

  • activeElement в shadow DOM возвращает текущий элемент с фокусом.
  • event.preventDefault() предотвращает стандартное поведение, например прокрутку страницы стрелками.
  • Преобразование NodeList в массив (Array.from) облегчает поиск индекса текущего элемента.

Поддержка клавиш активации

Элементы интерфейса должны реагировать на клавиши Enter и Space так же, как на клик мыши. Это повышает доступность:

_handleKeyDown(event) {
  if (event.key === 'Enter' || event.key === ' ') {
    this._activateElement(event.target);
    event.preventDefault();
  }
}

_activateElement(element) {
  console.log(`Элемент ${element.textContent} активирован`);
}

Важные моменты:

  • Для клавиши Space нужно использовать event.preventDefault(), иначе страница прокрутится.
  • Метод активации отделён от обработчика событий, что упрощает тестирование и повторное использование.

Обработка сложных сценариев

Для компонентов, таких как меню, списки или вкладки, часто требуется поддержка более сложной навигации:

  • Циклический фокус: при достижении последнего элемента фокус возвращается к первому.
  • Группы элементов: использование role="menu" или role="tablist" и aria-* атрибутов для улучшения совместимости с экранными читалками.
  • Сочетания клавиш: Ctrl + ArrowLeft/ArrowRight для перехода между вкладками или секциями.

Пример циклической навигации по списку:

_handleKeyDown(event) {
  const items = Array.from(this.shadowRoot.querySelectorAll('.item'));
  let currentIndex = items.indexOf(this.shadowRoot.activeElement);

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

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

Итоговые принципы

  • Все интерактивные элементы должны быть фокусируемыми (tabindex="0" или семантические элементы <button>, <a>).
  • Навигация по клавишам должна быть предсказуемой и логичной.
  • Использование Shadow DOM в Lit не мешает доступности, если правильно управлять фокусом и использовать aria-* атрибуты.
  • Разделение логики обработки клавиш и активации элементов повышает читаемость и повторное использование кода.

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