Как реализуется доступность JS?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Web Accessibility в JavaScript
Доступность (Accessibility, a11y) - это обеспечение того, что веб-приложение может использоваться людьми с различными ограничениями: нарушения зрения, слуха, моторики, когнитивные нарушения. JavaScript играет ключевую роль в реализации доступности на современных веб-сайтах.
WCAG 2.1 стандарты
Все рекомендации основаны на WCAG 2.1 (Web Content Accessibility Guidelines), которые определяют три уровня соответствия:
- A - минимальный уровень
- AA - рекомендуемый уровень
- AAA - максимальный уровень
Большинство проектов стремятся к уровню AA.
Семантический HTML - основание
Доступность начинается с правильного HTML, а JavaScript работает с ним:
<!-- ❌ Плохо - не семантично -->
<div onclick="handleClick()" role="button">
Кнопка
</div>
<!-- ✅ Хорошо - семантично -->
<button onclick="handleClick()">
Кнопка
</button>
<!-- ❌ Плохо - нет метки -->
<input type="text" />
<!-- ✅ Хорошо - есть метка -->
<label for="username">Имя пользователя</label>
<input id="username" type="text" />
ARIA атрибуты
ARIA (Accessible Rich Internet Applications) позволяет добавить информацию доступности к элементам, которые не имеют встроенной семантики.
Атрибут role
// Предоставляет информацию скрин-ридерам о роли элемента
<div role="alert" aria-live="assertive">
Ошибка: неверный email
</div>
<div role="navigation" aria-label="Основная навигация">
// навигация
</div>
<div role="tablist">
<button role="tab" aria-selected="true">Таб 1</button>
<button role="tab" aria-selected="false">Таб 2</button>
</div>
Атрибут aria-label
// Предоставляет текстовое описание элемента
<button aria-label="Закрыть диалог" onclick="closeDialog()">
<span aria-hidden="true">×</span>
</button>
// Полезно для иконок без текста
<button aria-label="Поиск" class="btn-search">
<svg><!-- иконка поиска --></svg>
</button>
Атрибут aria-live
// Уведомляет скрин-ридер об изменениях в режиме реального времени
<div aria-live="polite" aria-atomic="true">
Загрузка...
</div>
// Варианты aria-live:
// - off: не объявлять
// - polite: объявить когда пользователь свободен
// - assertive: объявить немедленно
Атрибут aria-describedby
<input
type="password"
aria-describedby="pwd-hint"
/>
<span id="pwd-hint">
Минимум 8 символов, содержащих букву и цифру
</span>
Управление фокусом с клавиатурой
Использователи с моторными нарушениями полагаются на навигацию с клавиатуры. JavaScript должен обеспечить нормальную работу Tab, Enter, Escape, стрелок.
// Правильно: используем встроенные элементы
<button>Кнопка</button> // Автоматически focusable
<input type="text" /> // Автоматически focusable
<a href="#">Ссылка</a> // Автоматически focusable
// Если нужен кастомный элемент, сделай его focusable
const customButton = document.createElement('div');
customButton.tabIndex = 0; // Добавляет элемент в порядок табуляции
customButton.role = 'button';
customButton.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick();
}
});
Управление фокусом в диалогах
// Важно управлять фокусом при открытии модального диалога
class Modal {
open() {
this.dialog.showModal();
// Переместить фокус в диалог
this.firstFocusableElement.focus();
}
close() {
this.dialog.close();
// Вернуть фокус на элемент, который открыл диалог
this.triggerButton.focus();
}
// Ловушка фокуса: удерживает фокус внутри диалога при Tab
trapFocus(e) {
if (e.key !== 'Tab') return;
const focusableElements = this.dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
}
Скрин-ридеры и объявления
// Объявить результат поиска для скрин-ридера
function performSearch(query) {
const results = searchAPI.search(query);
// Обновляем страницу
renderResults(results);
// Объявляем результат
const announcement = `Найдено ${results.length} результатов`;
announceToScreenReader(announcement);
}
function announceToScreenReader(message) {
const announcer = document.getElementById('sr-announcer');
announcer.textContent = message;
// Скрин-ридер объявит это сообщение из-за aria-live
}
HTML для объявлений:
<!-- Скрытый элемент для объявлений скрин-ридерам -->
<div
id="sr-announcer"
aria-live="polite"
aria-atomic="true"
class="sr-only"
></div>
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>
Контрастность и цвета
// JavaScript может проверять контрастность
function getContrastRatio(color1, color2) {
const lum1 = getRelativeLuminance(color1);
const lum2 = getRelativeLuminance(color2);
const lighter = Math.max(lum1, lum2);
const darker = Math.min(lum1, lum2);
return (lighter + 0.05) / (darker + 0.05);
}
// Проверяем соответствие WCAG
function isAccessibleContrast(ratio) {
// AA: 4.5:1 для обычного текста
// AAA: 7:1 для обычного текста
return ratio >= 4.5;
}
Тестирование доступности
// Использование инструментов для проверки
import { axe } from 'jest-axe';
import { render } from '@testing-library/react';
test('компонент доступен', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
// Ручная проверка:
// 1. Навигация только с клавиатуры (Tab, Enter, Escape)
// 2. Скрин-ридер (NVDA, JAWS, VoiceOver)
// 3. Zoom до 200%
// 4. Высокий контраст
// 5. Темный режим
Практический пример: Доступный dropdown
class AccessibleDropdown {
constructor(trigger, menu) {
this.trigger = trigger;
this.menu = menu;
this.items = menu.querySelectorAll('[role="menuitem"]');
this.isOpen = false;
this.init();
}
init() {
this.trigger.setAttribute('aria-haspopup', 'menu');
this.trigger.setAttribute('aria-expanded', 'false');
this.menu.setAttribute('role', 'menu');
this.trigger.addEventListener('click', () => this.toggle());
this.trigger.addEventListener('keydown', (e) => this.handleTriggerKey(e));
this.menu.addEventListener('keydown', (e) => this.handleMenuKey(e));
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
this.isOpen = true;
this.menu.hidden = false;
this.trigger.setAttribute('aria-expanded', 'true');
this.items[0].focus();
}
close() {
this.isOpen = false;
this.menu.hidden = true;
this.trigger.setAttribute('aria-expanded', 'false');
this.trigger.focus();
}
handleTriggerKey(e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
this.open();
}
}
handleMenuKey(e) {
const currentIndex = Array.from(this.items).indexOf(document.activeElement);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
const nextIndex = (currentIndex + 1) % this.items.length;
this.items[nextIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
const prevIndex = (currentIndex - 1 + this.items.length) % this.items.length;
this.items[prevIndex].focus();
break;
case 'Escape':
e.preventDefault();
this.close();
break;
}
}
}
Основные принципы доступности
- Воспринимаемость (Perceivable) - информация должна быть видна/слышна
- Операбельность (Operable) - интерфейс должен работать с клавиатурой
- Понятность (Understandable) - текст должен быть ясным, навигация логичной
- Надежность (Robust) - совместимость с вспомогательными технологиями
Итог
Доступность - это не дополнительный feature, а обязательная часть качественного веб-приложения. JavaScript должен:
- Сохранять семантику HTML
- Предоставлять ARIA информацию где необходимо
- Обеспечивать навигацию с клавиатуры
- Объявлять динамические изменения скрин-ридерам
- Управлять фокусом в интерактивных компонентах
Правильная реализация доступности помогает всем пользователям, включая людей с ограничениями.