Почему Accessibility — это не про "галочки", а про людей
Давайте честно: когда мы слышим "accessibility" (или a11y), многие из нас представляют бесконечные списки требований, скучные проверки и фразу "ну, это для людей с ограничениями". Но правда в том, что доступность — это про каждого. Вы когда-нибудь пытались заказать пиццу на ходу, держа телефон одной рукой? Или сидели в метро с плохим интернетом, где сайт грузится битый час? Вот это и есть проблемы доступности.
В этой статье мы поговорим о том, как TypeScript может стать вашим лучшим другом в создании доступных веб-приложений. Не просто "добавить теги", а построить архитектуру, где инклюзивность — это не фича, а фундамент.
TypeScript как щит от a11y-ошибок
Многие думают, что TypeScript — это только про типы данных и защиту от багов. Но на самом деле, строгая типизация может спасать и в вопросах доступности. Давайте разберем на примерах.
Типизация ARIA-атрибутов
Одна из самых частых проблем — неправильные значения ARIA-атрибутов. Вы когда-нибудь писали role="button" на div? А потом забывали добавить tabindex? TypeScript может отловить это на этапе компиляции.
// Плохо: никакой защиты
function createButton(label: string) {
return `<div role="button">${label}</div>`;
}
// Хорошо: типизируем ARIA
type AriaRole = 'button' | 'navigation' | 'alert' | 'dialog';
interface AriaAttributes {
role: AriaRole;
'aria-label'?: string;
'aria-expanded'?: boolean;
tabIndex?: number;
}
function createAccessibleButton(label: string, aria: AriaAttributes) {
// Теперь TypeScript проверит, что роль — валидная
return {
tag: 'div',
attrs: aria,
content: label
};
}
// Ошибка! 'btn' не входит в тип AriaRole
// createAccessibleButton('Нажми меня', { role: 'btn' });
Чувствуете разницу? TypeScript не даст вам случайно написать role="btutton" или забыть обязательный атрибут. Это как персональный ассистент, который шепчет: "Эй, а ты уверен, что это доступно?"
Строгая проверка фокусных состояний
Еще одна боль — управление фокусом. Особенно в SPA, где страница не перезагружается. TypeScript помогает создать безопасные обертки.
// Определяем, какие элементы могут получать фокус
type FocusableElement = HTMLInputElement | HTMLButtonElement | HTMLAnchorElement | HTMLSelectElement | HTMLTextAreaElement;
function safelySetFocus(element: FocusableElement | null): void {
if (element && document.contains(element)) {
element.focus();
} else {
console.warn('Элемент не найден в DOM или не может получить фокус');
}
}
// Использование
const modal = document.querySelector('.modal__close-btn');
safelySetFocus(modal as FocusableElement | null);
Обратите внимание: мы не просто вызываем .focus(), а проверяем, существует ли элемент и может ли он быть сфокусирован. Это предотвращает ошибки, которые могут оставить пользователя скринридера в пустоте.
Строим архитектуру с a11y в основе
Теперь перейдем к более продвинутым вещам. Как сделать так, чтобы accessibility была вшита в архитектуру вашего проекта, а не добавлена "на коленке" перед релизом?
Типизированные пропсы для компонентов
Создайте базовый интерфейс для всех ваших компонентов. Это как конституция для доступности.
// Базовый интерфейс для всех UI-компонентов
interface AccessibleComponentProps {
id?: string;
className?: string;
ariaLabel?: string;
ariaDescribedby?: string;
role?: string;
tabIndex?: number;
isDisabled?: boolean;
onClick?: () => void;
onKeyDown?: (e: React.KeyboardEvent) => void;
}
// Конкретный компонент кнопки
interface ButtonProps extends AccessibleComponentProps {
variant: 'primary' | 'secondary';
size: 'sm' | 'md' | 'lg';
children: React.ReactNode;
}
function AccessibleButton(props: ButtonProps) {
const { ariaLabel, children, isDisabled, onClick, ...rest } = props;
return (
<button
{...rest}
aria-label={ariaLabel || (typeof children === 'string' ? children : undefined)}
disabled={isDisabled}
onClick={!isDisabled ? onClick : undefined}
>
{children}
</button>
);
}
Зачем это? Теперь каждый новый компонент автоматически "наследует" требования к доступности. Вы не сможете создать кнопку без возможности добавить aria-label — TypeScript напомнит.
Управление состоянием для скринридеров
Часто мы скрываем контент, но забываем сказать об этом скринридеру. TypeScript поможет создать систему оповещений.
// Типы для состояний доступности
type LiveRegion = 'polite' | 'assertive' | 'off';
interface AriaLiveMessage {
message: string;
priority: LiveRegion;
duration?: number;
}
class AccessibilityManager {
private liveRegion: HTMLDivElement | null = null;
constructor() {
this.createLiveRegion();
}
private createLiveRegion(): void {
this.liveRegion = document.createElement('div');
this.liveRegion.setAttribute('aria-live', 'polite');
this.liveRegion.setAttribute('aria-atomic', 'true');
this.liveRegion.className = 'sr-only';
document.body.appendChild(this.liveRegion);
}
announce(message: string, priority: LiveRegion = 'polite'): void {
if (this.liveRegion) {
this.liveRegion.setAttribute('aria-live', priority);
this.liveRegion.textContent = ''; // Очищаем для повторного объявления
setTimeout(() => {
this.liveRegion!.textContent = message;
}, 100);
}
}
}
// Использование
const a11y = new AccessibilityManager();
a11y.announce('Форма успешно отправлена', 'assertive');
Это не просто код — это забота о пользователе. Представьте, что вы не видите экран, а просто слышите голос. Такая система сообщит вам о важных изменениях на странице, не заставляя гадать "а что там произошло?".
Реальные кейсы: где TypeScript спасает доступность
Давайте разберем три ситуации, с которыми сталкивается каждый разработчик.
1. Динамические списки и навигация с клавиатуры
interface ListItem {
id: string;
label: string;
isActive: boolean;
}
function AccessibleList({ items }: { items: ListItem[] }) {
const [focusedIndex, setFocusedIndex] = useState(-1);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusedIndex(prev => Math.min(prev + 1, items.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex(prev => Math.max(prev - 1, 0));
break;
case 'Enter':
case ' ':