Доступность
Клавиатурный контракт, стек оверлеев, живой регион и правила контраста — что библиотека гарантирует и чем это держится.
Доступность здесь не свойство отдельных компонентов, а контракт: одинаковые правила во всех, отклонения — оговорены, а держат их тесты, а не намерения. Компоненты реализуют паттерны WAI-ARIA APG; там, где поведение отличается, для отличия есть причина.
Клавиатура: четыре правила на всю библиотеку
- Tab ходит между виджетами, стрелки — внутри. Составной виджет — группа радиокнопок, дерево, вкладки, сегменты — занимает одну остановку в таб-порядке; внутрь попадают стрелками (roving tabindex).
- Esc закрывает верхнее. Все закрываемые оверлеи участвуют в одном стеке: Esc адресуется последнему открытому слою и не проваливается к нижним. Список, открытый внутри модалки, по Esc закрывает себя, а следующий Esc — модалку.
- Home и End — к краям. Везде, где есть список или диапазон. Их забывают чаще всего.
- Tab внутри открытой панели закрывает её.
GrDropdown,GrSelect,GrAutocompleteотдают фокус дальше по документу — пользователь не оказывается заперт в панели, которую не видит.
Enter и Space не взаимозаменяемы, и это правило нативных контролов, а не
вкусовщина:
| Элемент | Space | Enter |
|---|---|---|
| Кнопка, сегмент, заголовок аккордеона | активирует | активирует |
| Чекбокс | переключает | нет — нативный чекбокс сабмитит форму |
| Радиокнопка | выбирает | выбирает |
| Поле ввода в форме | пробел | сабмит формы |
Полная таблица по каждому компоненту — на его странице в каталоге: там перечислены клавиши, паттерн APG и оговорки.
Фокус и оверлеи
Модальный слой собран из шести примитивов — стек, ловушка фокуса, inert,
блокировка прокрутки, портал, присутствие в DOM, — и собран в одном месте, а
не в каждом компоненте. Причина прагматична: пропуск любого из инвариантов даёт
не поломку, а тихий дефект доступности.
- Ловушка держит фокус, только пока слой верхний. Иначе нижнее окно ворует
фокус у открытого поверх, а
inertгасит собственную панель. - Возврат фокуса принадлежит стеку, а не ловушке. Две системы возврата
подрались бы; поэтому у ловушки
restoreFocusвыключен. - Ловушка живёт без sentinel-узлов. Подход с двумя focus-guard кнопками по
краям кладёт интерактивные элементы внутрь контейнера с ARIA-ролью, и axe
ловит это как
nested-interactive. - Пока слой доигрывает уход, он не адресован указателю. Иначе клик в эти полторы десятых секунды достаётся исчезающему слою и пропадает бесследно.
Блокировка прокрутки — общий замок со счётчиком ссылок: несколько открытых оверлеев снимают её независимо от порядка закрытия, а ширина исчезнувшего скроллбара компенсируется, чтобы контент не дёргался.
Объявления для скринридера
Событие, о котором надо сказать, а показать негде, объявляет общий живой регион:
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 — переход на
логические свойства меняет вид каждого компонента и относится к мажорному
релизу, а не к патчу.