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

SkateJS — это библиотека для создания веб-компонентов, основанная на стандартах Web Components. Она предоставляет удобные средства для декларативного определения компонентов, их состояния и событий. Одним из ключевых аспектов интерактивных компонентов является клавиатурная навигация, которая обеспечивает доступность (accessibility) и улучшает взаимодействие пользователя с приложением без мыши.

Атрибуты и свойства для управления фокусом

SkateJS позволяет управлять фокусом элементов через стандартные HTML-атрибуты и события. Основные моменты:

  • tabindex — определяет порядок обхода элементов при навигации с помощью клавиши Tab.

    • tabindex="0" делает элемент фокусируемым в стандартном порядке.
    • tabindex="-1" делает элемент программно фокусируемым, но исключает его из последовательности Tab.
  • autofocus — автоматически устанавливает фокус на элемент при загрузке страницы или монтировании компонента.

В SkateJS свойства можно задать как через HTML-разметку компонента, так и через реактивное состояние компонента.

import { define, props, Component } from 'skatejs';

class MyButton extends Component {
  static props = {
    disabled: props.boolean
  }

  render() {
    return (
      <button tabindex="0" disabled={this.disabled}>
        Нажми меня
      </button>
    );
  }
}

define('my-button', MyButton);

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

Для полноценной клавиатурной навигации важна обработка событий keydown, keyup и keypress. SkateJS предоставляет удобный способ привязки обработчиков через метод connectedCallback или декларативно в render с использованием JSX.

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

class MenuItem extends Component {
  handleKeyDown = (event) => {
    switch (event.key) {
      case 'ArrowDown':
        this.focusNext();
        event.preventDefault();
        break;
      case 'ArrowUp':
        this.focusPrevious();
        event.preventDefault();
        break;
      case 'Enter':
        this.activate();
        break;
    }
  }

  focusNext() {
    const next = this.nextElementSibling;
    if (next) next.focus();
  }

  focusPrevious() {
    const prev = this.previousElementSibling;
    if (prev) prev.focus();
  }

  activate() {
    this.dispatchEvent(new CustomEvent('item-activated', { bubbles: true }));
  }

  render() {
    return (
      <div tabindex="0" onKeyD own={this.handleKeyDown}>
        {this.textContent}
      </div>
    );
  }
}

define('menu-item', MenuItem);

Использование ролей ARIA

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

  • role="menu" и role="menuitem" — для создания навигационных меню.
  • aria-selected — указывает, выбран ли элемент в списке.
  • aria-disabled — обозначает, что элемент недоступен для взаимодействия.

Пример списка с поддержкой ARIA:

class Menu extends Component {
  render() {
    return (
      <div role="menu">
        <menu-item role="menuitem">Пункт 1</menu-item>
        <menu-item role="menuitem">Пункт 2</menu-item>
        <menu-item role="menuitem">Пункт 3</menu-item>
      </div>
    );
  }
}

define('my-menu', Menu);

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

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

focusNext() {
  const items = Array.from(this.parentNode.querySelectorAll('[role="menuitem"]'));
  const index = items.indexOf(this);
  const nextIndex = (index + 1) % items.length;
  items[nextIndex].focus();
}

focusPrevious() {
  const items = Array.from(this.parentNode.querySelectorAll('[role="menuitem"]'));
  const index = items.indexOf(this);
  const prevIndex = (index - 1 + items.length) % items.length;
  items[prevIndex].focus();
}

Интеграция с состоянием компонента

Клавиатурная навигация тесно связана с реактивным состоянием компонента. SkateJS позволяет хранить активный элемент или индекс в свойствах и обновлять DOM при изменении состояния.

class TabPanel extends Component {
  static props = {
    activeIndex: props.number
  }

  handleKeyDown = (event) => {
    if (event.key === 'ArrowRight') {
      this.activeIndex = (this.activeIndex + 1) % this.children.length;
    } else if (event.key === 'ArrowLeft') {
      this.activeIndex = (this.activeIndex - 1 + this.children.length) % this.children.length;
    }
  }

  render() {
    return (
      <div onKeyD own={this.handleKeyDown}>
        {Array.from(this.children).map((child, i) =>
          <div tabindex={i === this.activeIndex ? "0" : "-1"}>{child.textContent}</div>
        )}
      </div>
    );
  }
}

define('tab-panel', TabPanel);

Рекомендации по улучшению навигации

  • Использовать tabindex только там, где это необходимо. Избыточные tabindex могут нарушить естественную последовательность.
  • Обрабатывать клавиши Enter, Space, стрелки, Home и End в зависимости от контекста.
  • Поддерживать ARIA-состояния, чтобы элементы были корректно восприняты вспомогательными технологиями.
  • Обновлять состояние компонента при изменении фокуса, чтобы реактивные интерфейсы оставались синхронизированными.

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