Создание доступных компонентов

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

В HTML часть доступности уже обеспечивается нативными элементами (button, input, select, a и др.). Однако при создании кастомных компонентов на основе div и span разработчик обязан самостоятельно воспроизводить семантику и поведение элементов. Именно здесь используются атрибуты ARIA (Accessible Rich Internet Applications).

Библиотека aria-query предоставляет программный доступ к спецификации ARIA: ролям, свойствам, состояниям и связям между ними. Это позволяет создавать инструменты анализа и генерации доступной разметки, проверять корректность использования ролей и строить компоненты, соответствующие стандартам доступности.


Назначение библиотеки aria-query

aria-query — это библиотека, содержащая структурированное представление спецификации ARIA. Она включает:

  • список ролей ARIA
  • допустимые атрибуты для каждой роли
  • взаимосвязи ролей
  • сопоставление HTML-элементов и ролей
  • ограничения и требования доступности

Библиотека используется многими инструментами проверки доступности, включая линтеры и анализаторы DOM.

Основные экспортируемые структуры:

  • roles
  • aria
  • elementRoles
  • roleElements
  • dom

Каждая из них представляет собой коллекции данных, описывающих правила доступности.


Установка библиотеки

Установка выполняется через пакетный менеджер:

npm install aria-query

Импорт в проекте:

import { roles, elementRoles, roleElements } from "aria-query";

Библиотека не взаимодействует напрямую с DOM. Она предоставляет только справочные данные.


Структура данных ролей ARIA

Коллекция roles содержит информацию о каждой роли ARIA.

Пример получения роли:

import { roles } from "aria-query";

const buttonRole = roles.get("button");

console.log(buttonRole);

Объект роли содержит несколько важных свойств:

Свойство Описание
props допустимые ARIA-атрибуты
requiredProps обязательные свойства
superClass родительские роли
accessibleNameRequired необходимость доступного имени

Пример структуры:

{
  props: {
    "aria-disabled": null,
    "aria-expanded": null,
    "aria-haspopup": null
  },
  requiredProps: {},
  superClass: [
    ["roletype", "widget", "command"]
  ]
}

Такая структура позволяет анализировать корректность применения роли.


Связь HTML элементов и ARIA ролей

Коллекция elementRoles определяет, какие роли автоматически назначаются HTML-элементам.

Пример получения ролей для элемента:

import { elementRoles } from "aria-query";

for (const [element, roles] of elementRoles) {
  console.log(element, roles);
}

Элемент представлен объектом:

{
  name: "button",
  attributes: []
}

А роли представлены множеством:

Set { "button" }

Таким образом библиотека хранит правила:

<button> → role="button"
<nav> → role="navigation"
<ul> → role="list"

Это важно при анализе избыточных ARIA ролей.


Обратное сопоставление: роли и HTML элементы

Коллекция roleElements позволяет определить, какие HTML элементы соответствуют конкретной роли.

Пример:

import { roleElements } from "aria-query";

const elements = roleElements.get("button");

console.log(elements);

Результат:

Set {
  { name: "button" },
  { name: "input", attributes: [{ name: "type", value: "button" }] },
  { name: "input", attributes: [{ name: "type", value: "submit" }] }
}

Это означает, что роль button уже реализована нативными элементами.

Использование role="button" на div допустимо, но менее предпочтительно, чем использование button.


Проверка корректности ролей

При создании кастомных компонентов необходимо проверять допустимость ролей.

Пример проверки существования роли:

import { roles } from "aria-query";

function isValidRole(role) {
  return roles.has(role);
}

Использование:

isValidRole("button"); // true
isValidRole("unknown"); // false

Это может применяться в системах статического анализа.


Проверка допустимых ARIA атрибутов

Каждая роль поддерживает ограниченный набор ARIA атрибутов.

Получение списка допустимых атрибутов:

const role = roles.get("checkbox");

const props = Object.keys(role.props);

console.log(props);

Результат:

[
  "aria-checked",
  "aria-readonly",
  "aria-required"
]

Такой анализ позволяет выявлять ошибки:

role="checkbox" aria-expanded="true"

aria-expanded не поддерживается ролью checkbox.


Создание доступной кастомной кнопки

Кастомная кнопка часто реализуется на основе div.

Пример:

<div role="button" tabindex="0">
  Отправить
</div>

Основные требования:

  1. Наличие роли
  2. Фокусируемость
  3. Обработка клавиатуры

JavaScript обработка:

const button = document.querySelector("[role='button']");

button.addEventListener("keydown", (event) => {
  if (event.key === "Enter" || event.key === " ") {
    event.preventDefault();
    button.click();
  }
});

Использование aria-query позволяет проверить допустимость роли.

if (!roles.has("button")) {
  throw new Error("Invalid ARIA role");
}

Создание доступного чекбокса

Нативный input type="checkbox" уже доступен. Однако иногда используется кастомная реализация.

HTML:

<div
  role="checkbox"
  aria-checked="false"
  tabindex="0"
>
  Подписаться
</div>

Изменение состояния:

const checkbox = document.querySelector("[role='checkbox']");

checkbox.addEventListener("click", toggle);

checkbox.addEventListener("keydown", (e) => {
  if (e.key === " ") {
    e.preventDefault();
    toggle();
  }
});

function toggle() {
  const checked = checkbox.getAttribute("aria-checked") === "true";
  checkbox.setAttribute("aria-checked", !checked);
}

Использование aria-query для проверки роли:

const role = roles.get("checkbox");

console.log(role.requiredProps);

Некоторые роли требуют обязательные свойства.


Проверка обязательных ARIA свойств

Некоторые роли требуют определённых атрибутов.

Пример:

const role = roles.get("slider");

console.log(role.requiredProps);

Результат:

{
  "aria-valuemin": null,
  "aria-valuemax": null,
  "aria-valuenow": null
}

Создание компонента slider без этих атрибутов нарушает правила доступности.

Пример корректного элемента:

<div
  role="slider"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuenow="50"
  tabindex="0"
></div>

Использование aria-query в линтерах

Библиотека широко используется в инструментах проверки доступности.

Например, правило ESLint может проверять:

  • существование роли
  • допустимость ARIA атрибутов
  • использование нативных элементов

Пример псевдоправила:

function validateRole(node) {
  const role = node.attributes.role;

  if (!roles.has(role)) {
    report("Unknown ARIA role");
  }
}

Проверка атрибутов:

function validateProps(roleName, attributes) {
  const role = roles.get(roleName);

  const allowedProps = Object.keys(role.props);

  attributes.forEach(attr => {
    if (attr.startsWith("aria-") && !allowedProps.includes(attr)) {
      report("Invalid ARIA attribute for role");
    }
  });
}

Анализ нативной семантики элементов

Использование ARIA не должно дублировать нативную семантику.

Неправильный пример:

<button role="button">Отправить</button>

Проверка через elementRoles:

function hasImplicitRole(elementName, role) {
  for (const [element, rolesSet] of elementRoles) {
    if (element.name === elementName && rolesSet.has(role)) {
      return true;
    }
  }
}

Если роль уже назначается автоматически, указывать её не требуется.


Генерация доступных компонентов

aria-query может использоваться для автоматического генератора компонентов.

Пример генерации структуры для роли:

function generateComponent(roleName) {
  const role = roles.get(roleName);

  return {
    role: roleName,
    requiredProps: role.requiredProps,
    supportedProps: Object.keys(role.props)
  };
}

Результат:

{
  role: "slider",
  requiredProps: [
    "aria-valuemin",
    "aria-valuemax",
    "aria-valuenow"
  ]
}

Такой механизм используется в системах дизайн-компонентов.


Создание доступного меню

Меню требует строгой структуры ролей.

Пример:

<div role="menu">
  <div role="menuitem" tabindex="0">Открыть</div>
  <div role="menuitem" tabindex="0">Сохранить</div>
</div>

Проверка роли:

roles.has("menu"); 
roles.has("menuitem");

Анализ поддерживаемых свойств:

const menuItem = roles.get("menuitem");

console.log(menuItem.props);

Роль menuitem поддерживает:

aria-disabled
aria-expanded
aria-haspopup
aria-checked

Архитектура доступных компонентов

При разработке компонентов рекомендуется соблюдать следующие принципы:

1. Использование нативных элементов

Предпочтение button, input, select.

2. Добавление ARIA только при необходимости

ARIA не должна заменять HTML.

3. Поддержка клавиатурной навигации

Основные клавиши:

  • Enter
  • Space
  • Arrow keys
  • Escape
  • Tab

4. Поддержка состояний

Через ARIA:

aria-expanded
aria-pressed
aria-selected
aria-hidden

5. Проверка допустимости ролей

Через aria-query.


Интеграция в систему дизайн-компонентов

aria-query может использоваться внутри библиотек компонентов:

  • React UI frameworks
  • Vue component libraries
  • Web Components

Библиотека позволяет:

  • проверять корректность ролей
  • валидировать ARIA атрибуты
  • генерировать документацию доступности
  • создавать инструменты анализа

Пример внутреннего валидатора:

export function validateAccessibility(role, props) {
  if (!roles.has(role)) {
    throw new Error("Invalid role");
  }

  const roleDefinition = roles.get(role);

  for (const prop of Object.keys(props)) {
    if (prop.startsWith("aria-") && !roleDefinition.props[prop]) {
      throw new Error(`Unsupported attribute ${prop}`);
    }
  }
}

Такая проверка помогает гарантировать, что создаваемые компоненты соответствуют стандартам доступности и спецификации ARIA.