Паттерны WAI-ARIA

Slim.js — это легковесный JavaScript-фреймворк для построения компонентов с декларативным синтаксисом. Интеграция с WAI-ARIA позволяет создавать доступные пользовательские интерфейсы, которые корректно воспринимаются вспомогательными технологиями: скринридерами, клавиатурной навигацией и другими инструментами доступности.

WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) — это набор атрибутов, которые описывают роль, состояние и свойства элементов интерфейса для повышения их доступности. В Slim.js эти атрибуты применяются напрямую к компонентам и их внутренним элементам через шаблоны и динамические привязки.


Атрибуты роли (role)

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

import Slim from 'slim-js';

class ButtonComponent extends Slim {
  get template() {
    return `
      <button role="button" @click="handleClick">
        <slot></slot>
      </button>
    `;
  }

  handleClick() {
    console.log('Button clicked');
  }
}

customElements.define('button-component', ButtonComponent);

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

  • role="button" явно указывает, что элемент интерактивен.
  • Для сложных виджетов (меню, вкладки, аккордеоны) роли определяют структуру доступности.

Статусы и состояния (aria-*)

WAI-ARIA предоставляет набор атрибутов aria-*, позволяющих отслеживать состояние компонентов: активность, свёрнутость, выделение.

class AccordionComponent extends Slim {
  isOpen = false;

  get template() {
    return `
      <div class="accordion">
        <button
          @click="toggle"
          aria-expanded="${this.isOpen}"
          aria-controls="panel1"
        >
          Заголовок аккордеона
        </button>
        <div id="panel1" aria-hidden="${!this.isOpen}">
          Контент аккордеона
        </div>
      </div>
    `;
  }

  toggle() {
    this.isOpen = !this.isOpen;
    this.update();
  }
}

customElements.define('accordion-component', AccordionComponent);

Особенности:

  • aria-expanded отражает, открыт ли элемент.
  • aria-hidden указывает, видим ли контент для вспомогательных технологий.
  • В Slim.js динамическое обновление состояния удобно через свойства компонента и метод update().

Взаимодействие с клавиатурой

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

class TabsComponent extends Slim {
  activeTab = 0;

  get template() {
    return `
      <div role="tablist">
        <button
          role="tab"
          aria-selected="${this.activeTab === 0}"
          tabindex="${this.activeTab === 0 ? 0 : -1}"
          @keydown="handleKeydown(0, $event)"
          @click="selectTab(0)"
        >Вкладка 1</button>
        <button
          role="tab"
          aria-selected="${this.activeTab === 1}"
          tabindex="${this.activeTab === 1 ? 0 : -1}"
          @keydown="handleKeydown(1, $event)"
          @click="selectTab(1)"
        >Вкладка 2</button>
        <div role="tabpanel" aria-hidden="${this.activeTab !== 0}">Контент 1</div>
        <div role="tabpanel" aria-hidden="${this.activeTab !== 1}">Контент 2</div>
      </div>
    `;
  }

  selectTab(index) {
    this.activeTab = index;
    this.update();
  }

  handleKeydown(index, event) {
    if (event.key === 'ArrowRight') this.selectTab((index + 1) % 2);
    if (event.key === 'ArrowLeft') this.selectTab((index + 2 - 1) % 2);
  }
}

customElements.define('tabs-component', TabsComponent);

Особенности:

  • tabindex управляет фокусируемостью.
  • aria-selected указывает выбранную вкладку.
  • Обработка стрелок позволяет перемещаться между элементами по стандартам ARIA.

Контейнеры и свойства родителя

Некоторые элементы требуют правильной вложенности и связей:

  • aria-controls связывает управляющий элемент с содержимым.
  • aria-labelledby связывает элемент с его заголовком.
  • role="group" или role="list" помогают определить семантику контейнера.
<div role="list" aria-label="Список задач">
  <div role="listitem">Задача 1</div>
  <div role="listitem">Задача 2</div>
</div>

Slim.js позволяет создавать такие списки с динамическим контентом через циклы:

class TaskList extends Slim {
  tasks = ['Задача 1', 'Задача 2'];

  get template() {
    return `
      <div role="list" aria-label="Список задач">
        ${this.tasks.map(task => `<div role="listitem">${task}</div>`).join('')}
      </div>
    `;
  }
}

customElements.define('task-list', TaskList);

Динамическая доступность

Slim.js упрощает динамическое управление ARIA-атрибутами:

  • Любое свойство компонента можно привязать к aria-*.
  • Использование метода update() автоматически перерисовывает шаблон и обновляет атрибуты.
  • Состояния компонентов могут синхронизироваться с пользовательскими событиями.
class CheckboxComponent extends Slim {
  checked = false;

  get template() {
    return `
      <div role="checkbox"
           aria-checked="${this.checked}"
           tabindex="0"
           @click="toggle"
           @keydown.space.prevent="toggle">
        ${this.checked ? '☑' : '☐'} Выбрать
      </div>
    `;
  }

  toggle() {
    this.checked = !this.checked;
    this.update();
  }
}

customElements.define('checkbox-component', CheckboxComponent);

Преимущества такого подхода:

  • Совмещение семантики и интерактивности в одном компоненте.
  • Полная поддержка клавиатурной навигации.
  • Простота поддержки и расширения.

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

  • Все интерактивные элементы должны иметь роль.
  • Состояния и свойства управляются через aria-*.
  • Динамическое обновление через Slim.js облегчает синхронизацию UI и доступности.
  • Клавиатурная навигация и правильная структура DOM критичны для корректного восприятия скринридерами.
  • Связи между элементами (aria-controls, aria-labelledby) создают логическую семантику интерфейса.

Slim.js и WAI-ARIA обеспечивают лёгкую интеграцию доступности в современные веб-компоненты без сложных обходных решений.