Доступность

Клавиатурный контракт, стек оверлеев, живой регион и правила контраста — что библиотека гарантирует и чем это держится.

Доступность здесь не свойство отдельных компонентов, а контракт: одинаковые правила во всех, отклонения — оговорены, а держат их тесты, а не намерения. Компоненты реализуют паттерны WAI-ARIA APG; там, где поведение отличается, для отличия есть причина.

Клавиатура: четыре правила на всю библиотеку

  1. Tab ходит между виджетами, стрелки — внутри. Составной виджет — группа радиокнопок, дерево, вкладки, сегменты — занимает одну остановку в таб-порядке; внутрь попадают стрелками (roving tabindex).
  2. Esc закрывает верхнее. Все закрываемые оверлеи участвуют в одном стеке: Esc адресуется последнему открытому слою и не проваливается к нижним. Список, открытый внутри модалки, по Esc закрывает себя, а следующий Esc — модалку.
  3. Home и End — к краям. Везде, где есть список или диапазон. Их забывают чаще всего.
  4. Tab внутри открытой панели закрывает её. GrDropdown, GrSelect, GrAutocomplete отдают фокус дальше по документу — пользователь не оказывается заперт в панели, которую не видит.

Enter и Space не взаимозаменяемы, и это правило нативных контролов, а не вкусовщина:

ЭлементSpaceEnter
Кнопка, сегмент, заголовок аккордеонаактивируетактивирует
Чекбокспереключаетнет — нативный чекбокс сабмитит форму
Радиокнопкавыбираетвыбирает
Поле ввода в формепробелсабмит формы

Полная таблица по каждому компоненту — на его странице в каталоге: там перечислены клавиши, паттерн APG и оговорки.

Фокус и оверлеи

Модальный слой собран из шести примитивов — стек, ловушка фокуса, inert, блокировка прокрутки, портал, присутствие в DOM, — и собран в одном месте, а не в каждом компоненте. Причина прагматична: пропуск любого из инвариантов даёт не поломку, а тихий дефект доступности.

  • Ловушка держит фокус, только пока слой верхний. Иначе нижнее окно ворует фокус у открытого поверх, а inert гасит собственную панель.
  • Возврат фокуса принадлежит стеку, а не ловушке. Две системы возврата подрались бы; поэтому у ловушки restoreFocus выключен.
  • Ловушка живёт без sentinel-узлов. Подход с двумя focus-guard кнопками по краям кладёт интерактивные элементы внутрь контейнера с ARIA-ролью, и axe ловит это как nested-interactive.
  • Пока слой доигрывает уход, он не адресован указателю. Иначе клик в эти полторы десятых секунды достаётся исчезающему слою и пропадает бесследно.

Блокировка прокрутки — общий замок со счётчиком ссылок: несколько открытых оверлеев снимают её независимо от порядка закрытия, а ширина исчезнувшего скроллбара компенсируется, чтобы контент не дёргался.

Объявления для скринридера

Событие, о котором надо сказать, а показать негде, объявляет общий живой регион:

ts
import { useAnnouncer } from '@feugene/granularity'

const { announce } = useAnnouncer()

announce('Link copied')
announce('Could not save', { politeness: 'assertive' })

Граница проходит по природе текста. Состояние виджета — «загружается», «ничего не найдено», «осталось 12 символов» — живёт своим регионом внутри виджета, рядом с содержимым. Событие — «тег удалён», «изображение 2 из 5» — уходит объявителю: своего места в разметке у него нет.

assertive прерывает текущую речь и уместен только там, где пропустить нельзя: отказ операции, потеря соединения. Ассертивный поток объявлений превращает страницу в неработающую — пользователь не успевает дослушать ни одно.

Контраст

Правило одно и жёсткое: насыщенный тон не используется как цвет текста. Каждому тону найдётся тема, где он проваливает AA на обычной подложке; парные роли -text держат AA везде. Подробности и таблица порогов — на странице темизации.

Проверяется это не глазами: один гейт ловит text-[var(--gr-<тон>)] в исходниках, второй пересчитывает контраст по токенам обеих тем — чтобы список запрещённых сочетаний не протух при первой же перекраске.

Уменьшенное движение

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

Там, где режим меняет не скорость, а сам сценарий — например, автопрокрутку стоит не ускорить, а не запускать, — компонент читает matchMedia сам.

Чем это проверяется

УровеньЧто проверяет
Юнит-тесты в jsdomОбработчики, роли, состояния, объявления
Живой браузерКлавиатуру и всё, чему нужен layout: в jsdom нет ни перемещения фокуса по Tab, ни активации по Enter, ни ResizeObserver
Гейты пакетаКонтраст токенов, prefers-reduced-motion, nested-interactive, отдельные инварианты оверлеев

Отдельный слой — сам этот портал: его страницы проходят axe в обеих темах, проверку клавиатурной доступности и требования WCAG 2.2 к отражению и размеру целей. Формальный статус соответствия — на странице заявления о доступности.

Чего библиотека не даёт

RTL не поддерживается. Компоненты размечены физическими направлениями, и dir="rtl" не читается нигде: документ справа налево получит зеркальные отступы, смещения и панели. Это осознанное решение линии 1.x — переход на логические свойства меняет вид каждого компонента и относится к мажорному релизу, а не к патчу.

Последняя ревизия: 2026-09-01