—От разметки компонентов к управлению фокусом—
1. Введение: различия в доступности между статическим HTML и средами React
Поскольку среда разметки для проектов SI (системной интеграции) переходит от JSP и статического HTML к структурам SPA (одностраничных приложений) на базе React и Vue, одной из крупнейших технических проблем, с которыми сталкиваются издатели, становятся аудиты сертификации качества веб-доступности (Web Accessibility, A11y).
В средах со статической разметкой большинство критериев сертификации можно было выполнить, правильно используя семантические теги в соответствии с веб-стандартами и успешно проходя валидацию. Однако в средах React, где страницы отображаются на основе виртуального DOM и динамического состояния, часто выявляются следующие дефекты доступности, даже если разметка написана по тем же стандартам.
-
При активации всплывающего модального слоя фокус клавиатуры с помощью Tab выходит в фоновой контент.
-
Хотя содержимое экрана обновилось в результате маршрутизации, программа чтения с экрана не распознаёт изменение и продолжает молчать.
-
При нажатии кнопки панель аккордеона открывается, но состояние открытия или закрытия не сообщается вспомогательным устройствам голосом.
-
Когда к кнопке, содержащей только значок, применяются стили, программа чтения с экрана полностью пропускает текст, скрытый этими стилями.
В большинстве проектов SI проверки качества веб-доступности сосредоточены на октябре и ноябре, перед запусками в конце года. Если в этот период возникают дефекты управления фокусом или интеграции атрибутов ARIA, приходится изменять не только разметку, но и проектирование состояния компонента, что приводит к серьёзным задержкам поставки.
Поэтому работу по включению основных правил в разметку необходимо начинать уже на этапе разработки компонентов.
2. Основные правила синтаксиса доступности в среде React JSX
Поскольку JSX (JavaScript XML) в React создаёт объекты виртуального DOM вместо прямого управления DOM браузера, необходимо понимать его явные синтаксические отличия от стандартного написания HTML.
2.1. Сохранение дефисов (kebab-case) в атрибутах aria-*
В JSX большинство атрибутов HTML записывается в camel case, чтобы избежать конфликтов с зарезервированными словами(className, htmlFor, tabIndex). Однако атрибуты aria-*, определённые спецификацией WAI-ARIA, и атрибуты data-* должны сохранять форму с дефисами (kebab-case) для соответствия веб-стандартам. Если ошибочно записать их в camel case, браузер сочтёт их недопустимыми атрибутами, из-за чего программы чтения с экрана не смогут их интерпретировать.
// ❌ 잘못된 작성 방식: 속성명 오타 및 role 남발
<div tabindex="0" role="button" ariaHidden="false" onClick={handleClick}>
상세 보기
</div>
// ⭕ 올바른 작성 방식: 네이티브 버튼 태그 및 웹 표준 하이픈 표기
<button type="button" tabIndex={0} aria-hidden="false" onClick={handleClick}>
상세 보기
</button>
При использовании тега <button> взаимодействие с клавиатурой при нажатии клавиш Enter и Space изначально поддерживается движком браузера, поэтому нет необходимости реализовывать отдельные обработчики событий клавиатуры для тега <div>.
2.2. Предотвращение ненужных обёрток <div> и нарушений семантической структуры
Компоненты React должны возвращать один корневой узел. Если по привычке оборачивать верхний уровень компонента в <div className="wrapper">, семантическая иерархия нарушается.
В частности, если <li> не является непосредственным дочерним элементом <ul> или <ol>, либо если <dt> и <dd> не являются непосредственными дочерними элементами <dl> из-за промежуточного <div>, программа чтения с экрана не сможет правильно вычислить общее количество элементов списка и текущую позицию.
// ❌ <ul> 직하위에 <div>가 끼어들어 리스트 시맨틱이 단절된 형태
const Breadcrumb = () => (
<ul className="breadcrumb">
<div className="inner">
<li>홈</li>
<li>공지사항</li>
</div>
</ul>
);
// ⭕ React.Fragment(<>...</>)를 활용하여 실제 DOM 오염을 방지한 형태
const Breadcrumb = () => (
<ul className="breadcrumb">
<>
<li>홈</li>
<li>공지사항</li>
</>
</ul>
);
Использование <React.Fragment> (сокращённый синтаксис <>...</>) позволяет сохранить правильную иерархию, не добавляя ненужные теги в фактическое дерево отображения браузера.
3. Основные аспекты для разных типов инвалидности (нарушения зрения и слуха)
Чтобы соответствовать стандартам руководств по веб-доступности (KWCAG 2.2), необходимо учитывать сенсорные среды, через которые пользователи воспринимают веб-содержимое и взаимодействуют с ним, при разработке компонентов.
-
Люди с нарушениями зрения (слепые и слабовидящие пользователи): поскольку они не могут пользоваться мышью, они полностью полагаются на клавиатуру (Tab, Shift + Tab, клавиши со стрелками) и голосовой вывод программы чтения с экрана. Все интерактивные элементы должны поддерживать фокусировку с клавиатуры, а визуальная информация всегда должна преобразовываться в альтернативный текст (alt, aria-label, методы IR). Кроме того, динамические изменения экрана, возникающие в результате асинхронного обмена данными, должны объявляться с помощью свойств голосового прерывания в реальном времени.
-
Люди с нарушениями слуха (глухие и слабослышащие пользователи): необходимо предоставлять синхронизированные субтитры (<track> в соответствии со спецификацией WebVTT), содержащие не только диалоги из видеоматериалов, но и информацию о фоновой музыке и звуковых эффектах. Кроме того, обратная связь, предоставляемая только посредством звука, например сигнал ошибки или уведомление о завершении обработки, должна одновременно отображаться в виде визуального текста и всплывающих уведомлений.
4. Практические шаблоны доступности для четырёх основных компонентов пользовательского интерфейса
4.1. Компонент скрытого текста только для программ чтения с экрана (VisuallyHidden)
Стили display: none или visibility: hidden, применяемые к кнопкам со значками, удаляют элементы из дерева отображения браузера, не позволяя программам чтения с экрана также их прочитать. Компонент IR (замены изображения), который полностью передаёт информацию вспомогательным устройствам, оставаясь визуально скрытым, следует стандартизировать и использовать во всём приложении.
// components/common/VisuallyHidden.jsx
import React from 'react';
const hiddenStyle = {
position: 'absolute',
width: '1px',
height: '1px',
padding: '0',
margin: '-1px',
overflow: 'hidden',
clip: 'rect(0, 0, 0, 0)',
whiteSpace: 'nowrap',
border: '0',
};
export const VisuallyHidden = ({ children, as: Tag = 'span' }) => (
<Tag style={hiddenStyle}>{children}</Tag>
);
// 활용 예시: 닫기 아이콘 버튼
export const CloseButton = ({ onClick }) => (
<button type="button" onClick={onClick} className="icon-btn">
<svg aria-hidden="true" focusable="false">{/* 아이콘 그래픽 */}</svg>
<VisuallyHidden>팝업 레이어 닫기</VisuallyHidden>
</button>
);
Запрещайте чтение ненужной графики SVG, применяя aria-hidden="true", и передавайте пользователям вспомогательных технологий точное назначение действия с помощью заключённого внутри текста VisuallyHidden.
4.2. Компонент аккордеона: синхронизация значений состояния с WAI-ARIA
Пользовательские интерфейсы с переключением и состояниями открытия и закрытия должны напрямую сопоставлять состояние useState в React с атрибутами WAI-ARIA.
// components/ui/Accordion.jsx
import React, { useState } from 'react';
export const AccordionItem = ({ id, title, content }) => {
const [isOpen, setIsOpen] = useState(false);
const headerId = `acc-header-${id}`;
const panelId = `acc-panel-${id}`;
return (
<div className="accordion-item">
<h3>
<button
type="button"
id={headerId}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setIsOpen((prev) => !prev)}
>
{title}
<span aria-hidden="true">{isOpen ? '▲' : '▼'}</span>
</button>
</h3>
<div
id={panelId}
role="region"
aria-labelledby={headerId}
hidden={!isOpen}
>
<p>{content}</p>
</div>
</div>
);
};
-
aria-expanded={isOpen}: свяжите логическое значение состояния, чтобы программа чтения с экрана автоматически объявляла текущее состояние кнопки как «раскрыто» или «свернуто».
-
aria-controls и aria-labelledby: свяжите кнопку-триггер и фактическую панель содержимого с помощью явных значений ID, чтобы определить их взаимосвязь.
-
hidden={!isOpen}: примените нативный атрибут hidden, чтобы в закрытом состоянии содержимое безопасно исключалось как из отображения экрана, так и из дерева доступности.
4.3. Модальный компонент: удержание и восстановление фокуса
Модальные компоненты подвергаются наиболее строгой оценке во время аудитов качества доступности. Сочетая useRef и useEffect, необходимо полностью контролировать циклическое перемещение фокуса клавиатуры внутри модального окна и восстанавливать фокус на предыдущем элементе после его закрытия.
// components/ui/AccessibleModal.jsx
import React, { useEffect, useRef } from 'react';
export const AccessibleModal = ({ isOpen, onClose, title, children }) => {
const modalRef = useRef(null);
const prevFocusRef = useRef(null);
useEffect(() => {
if (!isOpen) return;
// 1. 모달 활성화 전 현재 초점 요소 저장
prevFocusRef.current = document.activeElement;
// 2. 모달 내부 초점 가능 요소 탐색 및 첫 번째 요소로 초점 이동
const focusable = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable?.length) focusable[0].focus();
// 3. Tab 키 순환(Focus Trap) 및 ESC 닫기 제어
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
return;
}
if (e.key === 'Tab' && focusable?.length) {
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
window.addEventListener('keydown', handleKeyDown);
// 4. 언마운트 또는 닫힘 시 기존 초점 복귀
return () => {
window.removeEventListener('keydown', handleKeyDown);
prevFocusRef.current?.focus();
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-backdrop" onClick={onClose}>
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="modal-box"
onClick={(e) => e.stopPropagation()}
>
<h2 id="modal-title">{title}</h2>
<div className="modal-content">{children}</div>
<button type="button" onClick={onClose}>닫기</button>
</div>
</div>
);
};
Назначение role="dialog" и aria-modal="true" позволяет вспомогательным технологиям распознать текущий экран как диалоговый слой, а внутренний цикл фокуса предотвращает критический дефект выхода фокуса клавишей Tab в фоновый контент.
4.4. Динамические уведомления в реальном времени (aria-live) и синхронизированные субтитры видео (track)
Когда сообщение об ошибке отображается в части экрана в результате асинхронного обмена данными SPA, для уведомления пользователей с нарушениями зрения в реальном времени необходимо применить атрибут aria-live. Кроме того, видеоматериалы должны содержать субтитры стандарта WebVTT, чтобы обеспечить доступность для людей с нарушениями слуха.
// 실시간 폼 에러 낭독 컴포넌트
export const FormAlert = ({ errorMsg }) => {
if (!errorMsg) return null;
return (
<div role="alert" aria-live="assertive" className="error-box">
<span aria-hidden="true">⚠️</span>
<p>{errorMsg}</p>
</div>
);
};
// 청각장애인을 위한 WebVTT 자막 비디오 컴포넌트
export const VideoPlayer = ({ videoSrc, subtitleSrc, title }) => (
<video controls aria-label={title}>
<source src={videoSrc} type="video/mp4" />
<track kind="subtitles" src={subtitleSrc} srcLang="ko" label="한국어 자막" default />
</video>
);
aria-live="assertive" немедленно прерывает текущее объявление при отображении ошибки и объявляет сообщение об ошибке с наивысшим приоритетом. Для обычных информационных сообщений используйте aria-live="polite", который не нарушает процесс чтения пользователя.
5. Основная проверка перед развёртыванием: трёхэтапный рабочий процесс самопроверки
Это трёхэтапная процедура проверки, предотвращающая появление дефектов в процессе работы без ожидания обратной связи от аудиторского агентства после завершения разработки.
-
Навигация только с клавиатуры (тест без мыши): перемещайтесь по всему экрану, используя только клавиши Tab, Shift + Tab, Enter и Space, не пользуясь мышью. Проверьте, хорошо ли виден контур фокуса невооружённым глазом и нормально ли перемещается фокус внутри слоёв всплывающих окон.
-
Запустите инструменты автоматической диагностики: запустите axe DevTools или Chrome Lighthouse в инструментах разработчика браузера, чтобы автоматически проверить контрастность текста и фона (минимум 4.5:1), наличие отсутствующих атрибутов alt у изображений и сопоставление элементов управления форм через id-htmlFor.
-
Прослушайте интерфейс с помощью встроенной программы чтения с экрана ОС: запустите VoiceOver в macOS (Cmd + F5) или NVDA в Windows (бесплатно) и перемещайтесь по компонентам с закрытыми глазами. Проверьте, чётко ли объявляется назначение кнопок со значками на корейском языке и начинает ли модальное окно корректно читать свой заголовок при открытии.
6. Заключение: доступность — ключевая компетенция издателя
Сайты проектов SI всегда работают в условиях жёстких сроков. В такой среде отношение к веб-доступности как к «второстепенной задаче, для которой достаточно избежать вычетов непосредственно перед запуском» ставит под угрозу весь график из-за переделок в последний момент.
Если издатели с раннего этапа разметки компонентов React создадут стандартную семантическую структуру, свяжут значения состояния с атрибутами ARIA и спроектируют стабильные сценарии перемещения фокуса с клавиатуры, общее качество проекта и продуктивность разработки значительно повысятся.
sangsuni