Практическое руководство по веб-доступности (A11y) для издателей SI

Практическое руководство по веб-доступности (A11y) для издателей SI

—От разметки компонентов к управлению фокусом—

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. Основная проверка перед развёртыванием: трёхэтапный рабочий процесс самопроверки

Это трёхэтапная процедура проверки, предотвращающая появление дефектов в процессе работы без ожидания обратной связи от аудиторского агентства после завершения разработки.

  1. Навигация только с клавиатуры (тест без мыши): перемещайтесь по всему экрану, используя только клавиши Tab, Shift + Tab, Enter и Space, не пользуясь мышью. Проверьте, хорошо ли виден контур фокуса невооружённым глазом и нормально ли перемещается фокус внутри слоёв всплывающих окон.

  2. Запустите инструменты автоматической диагностики: запустите axe DevTools или Chrome Lighthouse в инструментах разработчика браузера, чтобы автоматически проверить контрастность текста и фона (минимум 4.5:1), наличие отсутствующих атрибутов alt у изображений и сопоставление элементов управления форм через id-htmlFor.

  3. Прослушайте интерфейс с помощью встроенной программы чтения с экрана ОС: запустите VoiceOver в macOS (Cmd + F5) или NVDA в Windows (бесплатно) и перемещайтесь по компонентам с закрытыми глазами. Проверьте, чётко ли объявляется назначение кнопок со значками на корейском языке и начинает ли модальное окно корректно читать свой заголовок при открытии.

6. Заключение: доступность — ключевая компетенция издателя

Сайты проектов SI всегда работают в условиях жёстких сроков. В такой среде отношение к веб-доступности как к «второстепенной задаче, для которой достаточно избежать вычетов непосредственно перед запуском» ставит под угрозу весь график из-за переделок в последний момент.

Если издатели с раннего этапа разметки компонентов React создадут стандартную семантическую структуру, свяжут значения состояния с атрибутами ARIA и спроектируют стабильные сценарии перемещения фокуса с клавиатуры, общее качество проекта и продуктивность разработки значительно повысятся.

sangsuni

Site footer