Немного обо мне
Я занимаюсь frontend уже 8 лет и с самого начала карьерного пути наблюдал как наш продукт рос и развивался (и я вместе с ним), формировалось frontend-сообщество, мировые web-стандарты получали второе дыхание и быстрое развитие, а accessibility приходило в наши дома. За это время я успел достаточно глубоко проникнуться идеями доступности, поскольку ранее и сам уделял внимание клавиатурному вводу в личном пользовании интернетом. Полагаю, все знакомы со стандартным паттерном аутентификации: зашли на сайт, ввели логин, нажали tab, ввели пароль, enter – и вот, вы вошли. Развивая в этом направлении себя, команду и продукты, я пришёл к роли эксперта по web-доступности, делясь знаниями и причинами, почему нам нужно уметь лучше работать с фокусом и html. Данной статьёй я планирую рассказать про интересные случаи и про то, в каких ситуациях можно оказаться, постигая новые горизонты, но не собираюсь даже пытаться заменить справочники по aria-атрибутам и эталонным примерам доступных компонентов. Стандарт W3C полон полезной информации, которая сдержанно подаёт только нужное. Я же предостерегу вас от некоторых сомнительных решений, с которыми вы можете столкнуться, и постараюсь приблизить задачу доступности к стандарту, коим для нас сейчас является покрытие тестами и использование анализаторов кода. Итак, приступим.
Предисловие
Сразу оговорюсь, что отправной точкой стал стандарт WCAG (Web Content Accessibility Guidelines). Также в последующем появился основанный на нём ГОСТ 52872-2019. Рекомендации WCAG – здорово, и им стоит следовать. ГОСТ же помог нам повысить приоритет внедрения доступности.
Все начиналось в далёком 2017 году. Я только вышел из продуктовой команды и попал в платформу (на тот момент скорее в core-команду), быстро ухватившись за библиотеку базовых компонентов, которая только-только начинала зарождаться. В тот момент мы и думать не планировали о доступности (accessibility, a11y), оставляя ее специальным режимам "под кнопку" на когда-нибудь потом.
А потом концепт изменился. Зачем очередной неинтересный переключатель, если можно сразу сделать хорошо, не тратя ресурсы на поддержку, по сути, двух версий? Так мы и решили, познакомившись с крутыми ребятами из подразделения, лидирующего процесс улучшения доступности всех цифровых продуктов экосистемы. Они заложили нам в беклог зерно, из которого по сей день продолжает расти прекрасный цветок.
Вот только кодовая база на добрых 30+ компонентов уже была готова. А, как вы знаете, любой контракт с бизнесом и командой о внедрении новой практики состоит из:
пощупать,
восхититься,
через боль и слезы начать внедрять,
словить кучу хейта к себе и окружающим, задаваясь вопросом, зачем это всё,
поймать мазохистический кайф под конец работ,
проникнуться технологией окончательно после внедрения,
обложиться метриками,
все потерять на крупном релизе,
снова внедрять опытными руками,
подстраиваться и адаптироваться,
стать адептом и пилить вовеки.
Щуп и восхищение
Собственно, таков был наш путь введения практики делать весь UI доступным. Отправной точкой для нас было использование семантики HTML5, поэтому в крупную клетку все заголовки и секции были уже доступны для быстрой навигации и использования.
На этапе изучения документации W3C, просмотра примеров и изучения исходного кода react-компонентов из opensource мы смогли выделить следующие векторы движения:
дизайн – контрастность, тексты, упрощение UI,
доступность компонентов вывода,
доступность компонентов ввода,
тестирование ручное,
тестирование автоматизированное.
Прежде, чем разбирать данные пункты, хочу отметить, что доступность – понятие обширное (как интернационализация на фоне локализации). Одними screen reader (программы экранного доступа, например, VoiceOver, JAWS, NVDA) тут не обойдёшься. Необходимо заботиться также и о людях без гибких средств ввода, без хороших контрастных мониторов, с подсевшим зрением или стремящихся к масштабу страницы 150%. Мы сразу определились, что, если существует управление с клавиатуры без использования мыши, мы также считаем это задачей доступности.
С первым пунктом расправиться было легко. Все эти моменты прямо пересекались с обычными пользователями, желающими получить простой и удобный сервис, который не режет глаз. Дизайнеры быстро освоились с новыми правилами реализации макетов, причесав палитру цветов и приняв как данность рамку состояния focused. Более того, им понравилась идея менять толщину рамки, чтобы не только цветом, но и размером показывать изменение состояния элементов в фокусе.
Пример кроссбраузерного кода для выделения рамки фокуса с клавиатуры (класс .pointer-events устанавливается полифиллом, слушающим события mouse):
export const focusVisible = (style: SerializedStyles): SerializedStyles => css`
body:not(.pointer-events) &:focus {
${style}
}
&:focus-visible {
${style}
}
`
В целом, поддержка любых дополнительных средств ввода/вывода и есть доступность. Корректная реализация задач accessibility ведет продукты к доступности для максимально широкого круга потребителей.
Доступность компонентов вывода оказалась тоже не самой сложной задачей, поскольку изучение aria-* и role (дополнительные атрибуты HTML, усиливающие и/или описывающие доступность) нам подсказали, что достаточно их просто рассыпать по компонентам и немного попотеть с динамикой (например, в какой момент alert должен перехватывать фокус screen reader?).
Истинным испытанием для нас стали компоненты ввода. На голых демо (кстати, в нашем курсе по доступности [а об этом в разделе «Доступность как религия»] за базовый пример брался выпадающий Listbox) реализация выглядела просто и лаконично: тут атрибуты статичные, тут динамичные, подсвети строку клавиатурного фокуса, да фокус перебрось на элемент выборки. А по факту же с композиционной архитектурой компонентов, когда разработчик собирает строку ввода, опции, группы, overlay как кусочки конструктора с тонкой настройкой поведения каждого, мы пришли к модификации передаваемых свойств через React.Children, ряду ограничений и лишений фич доступности. Как итог таких изысканий – дополнительная реализация компонента Listbox со 100% покрытием доступностью в одном ряду с Dropdown, который довольствовался тем, что мы ему смогли дать. Кстати, Listbox с тех пор претерпел изменения спецификации доступности, и нам снова пришлось всё переделывать, вставляя role=combobox на "горячую".
Ручное тестирование в условиях, когда вся команда сидит на MacOS ради шикарной работы файловой системы, мы проводили в VoiceOver. Самые запоминающиеся проблемы: символ рубля, читаемый как ruble sign и табуляция только по определенным элементам управления в Safari. Обе проблемы, кстати, исправляются настройкой screen reader и браузера соответственно исключительно самим пользователем. Однако, пока мы решали данную проблему и приходили к простому выводу с настройкой, мы улучшили общий UX, добавив больше фишек для работы с денежными суммами (например, копирование текста как код валюты).
В будущем нам ещё предстояло придумать, как добавить автоматизации в тестирование accessibility.
Как доступность укладывали поверх асфальта
Уже упомянутый Listbox – история, которая научила нас принимать свои ошибки, разделяя будущее и прошлое, не мучая истлевший труп плохо спроектированного компонента. Да, на этом этапе я хочу подчеркнуть одну важную мысль: удобство покрытия доступностью компонента – мерило корректности его архитектуры и масштабируемости. В будущем мы ещё вспомним об этом.
На данном этапе мы условились, что у всех пользователей должен быть максимально простой и прямой способ ввода, он должен быть доступным, а все остальное – скрыто. Ввод даты по маске, но без доступного календаря. Тогда мы считали, что таблица кнопок слишком жестокое решение для наших пользователей. Слайдер сопровождён полем ввода точной величины, а сам слайдер – скрыт. Кстати, мы покрыли слайдер доступностью ещё на этапе исследования, но отложили реализацию в дальний ящик, а когда достали обратно – задача была решена за 15 минут.
Трудности встречались даже в самых простых компонентах – обычный тег img. Мы знали, что картинки стоит сопроводить прошедшим локализацию читаемым alt, но также включали для декоративных иллюстраций role-атрибут presentation, который вступал в конфликт с alt. Только с опытом мы пришли к тому, что alt в таких случаях достаточно оставлять пустым, поскольку полезную нагрузку на пользователя подобные иллюстрации не несли.
Автоматизированное тестирование мы решили проводить в Axe (анализатор доступности HTML), помня, что данный инструмент предпочитает анализировать голые и статичные HTML и CSS, игнорируя такие вещи, как клавиатурный ввод. Но в то время мы активно внедряли UI-тесты с webdriverio (framework автотестирования на node.js), поэтому знали, что динамика ложится на плечи наших сценариев. Сейчас данная фича перекочевала в нашу конфигурацию cypress.
Относительно большим успехом для нас стало внедрение в UI-тесты этапа A11Y-тестов в CI этапе сборки. Суть реализации заключалась в том, чтобы сценарий отправил код Axe в браузер, запустил его там и вернул все vulnerabilities, и так для каждого состояния. Управление Axe на таком уровне позволило нам принимать в исключения, казалось бы, критичные моменты, например, дублированные ID у SVG (что нехорошо, но в моменте доступности не вредило), и строить более разумные шлагбаумы для доступных компонентов.
Перестраиваться больно
В определенный момент, когда мы добрались до доступности роутинга приложения и Workflow (движок построения форм со state-машиной на бэке, чья особенность в изменении верстки всей страницы без изменения её адреса при переходе на следующий шаг формы), мы начали сталкиваться с задачами, гайдов для которых нет нигде. Из имеющихся инструментов мы использовали два правила:
браузерный фокус цепляет за собой фокус screen reader,
aria-label (заголовок элемента, доступная только screen reader) не позор, если есть задача донести информацию до пользователя, но других возможностей не представлено.
Таким образом, переходя на новый шаг заполнения анкеты, мы переопределяли фокус на неинтерактивный элемент в начале формы и прописывали ему aria-label, чтобы клиент понимал, в каком месте процесса он оказался.
Ниже по коду представлено множество useEffect в движке Workflow, чтобы обыграть все возможные состояния.
const a11yFocus = (): void => {
pseudoTitle.current?.focus({ preventScroll: true })
}
useEffect((): void => {
if (isLoading)
return
}
if (scrollTo && typeof scrollTo === 'string') {
scrollToScreenElement(scrollTo)
} else {
scrollToElement(form.current)
a11yFocus()
}
}, [isLoading])
useEffect((): void => {
scrollToError(formErrors)
}, [submitCount])
useEffect((): void => {
a11yFocus()
scrollToTop(hasProcessError)
}, [hasProcessError, stepTitle, messages?.length])
Но если эта задача была решена, то следом были ещё два пункта для хейта в библиотеке компонентов, от которых хотелось бросить всё и продолжить делать по классике, как знаем:
передача компонентам атрибутов доступности
и доступность прикладных команд.
Изначально мы полагали использовать обязательный объект a11y с содержимым, описывающим условные aria-label и alt или настраивающим aria-busy. Но довольно быстро мы пришли к понимаю неудобства такого подхода. Мы постарались минимизировать и стандартизировать вынос такого API на публику, оставив наиболее простые атрибуты на откуп потребителям в тех названиях, в которых они задаются в HTML.
Процесс же обучения занимал достаточно много времени, а обоснованно донести до продуктовой команды, что у неё баг в Dropdown, и надо перейти на Listbox, порой было довольно сложно. Однако большинство людей восприняли задачу доступности весьма тепло, как вызов, прикладывая Axe повсеместно, как в ручном тестировании, так и в автоматизированном.
Сегодня мы говорим сторонним командам, что они вольны выбирать, делать ли компоненты доступнее и использовать предлагаемые дизайн-системой решения, но просим их не забывать, что рано или поздно, доступность может прийти даже в самую специфичную админку.
Наташа, мы все уронили
Встав на рельсы, мы успешно развивали тот самый старый наш дизайн, пока не пришёл новый концепт дизайна, полностью меняющий стилистику, добавляющий "воздух" в компоненты и аналог Single SPA в нашу архитектуру (у нас собственная реализация микромодульности, но об этом можно как-нибудь в другой раз), и вместе с этим меняющий обычную библиотеку компонентов на дизайн-систему. Мы так увлеклись изменениями, что позабыли про соблюдение сохранения доступности, автотесты временно отключились для быстро переделываемых компонентов и вовсе перешли на cypress. Спустя несколько лет внедрений и развития, доступность опустилась до оценочного значения в 30%.
Не веря, что мы действительно уронили доступность, мы пошли по верхам, чиня процессы в новом дизайне, но этого оказалось недостаточно. К нам на помощь вновь пришли наши коллеги – большие эксперты доступных технологий с реальным боевым опытом эксплуатации в условиях ограниченных возможностей. Было решено поставить на конвейер следующие пункты:
освоение маководами NVDA, JAWS, их конфигурирование и связывание с браузерами, ведь большинство пользователей использует ОС Windows, а данный софт доступен именно под неё, и также наблюдались отличия между screen readers аналогично тому, как они наблюдаются между разными браузерами,
постоянное участие и контроль со стороны экспертов доступности через бизнес-процессы и демо-страницы компонентов дизайн-системы,
создание специальных тест-кейсов тестировщиками и их плотное участие во всем процессе.
Уже имея опыт адаптации и прошлые наработки, мы раскачали эффективность восстановления доступности, закрыв почти 100% задач за 1 квартал. До сих пор, пожалуй, постреливает только виртуализация в Listbox, с чем мы активно боремся.
На этом этапе красивым бизнес-процессам по-прежнему хотелось забыть о доступности и выключить поля ввода для Slider, но нам удалось договориться и заменить поле текстового ввода на мой любимый Listbox (вы уже могли видеть, как это выглядит, в иллюстрациях выше). Решение красиво и лаконично вписалось, и удовлетворило всех.
И почему-то ранее мы не так сильно обращали внимание на наш расширенный компонент на основе тега label с заголовком, описанием, подсказкой и ошибкой поля ввода. Расстановка всех связей поля ввода с его описательной частью через aria-<something>edby вызывало у разработчиков восторг, что всё получается правильным и читаемым самым логичным образом.
Новые решения во славу двум буквам и числа 11
Развиваясь и используя a11y как инструмент, а не как задачу, нам удалось внедрить ещё ряд практик и компонентов, реализующих изысканные пожелания любых наших клиентов. Самое классическое из них – скрытый стилями текст (visually hidden), читаемый через screen reader в нужном месте без имеющихся для того отображаемых элементов. Мы очень долго стремились к полному подобию читаемого отображаемому, но в итоге пришли к выводу, что всё же завести вспомогательный visually hidden компонент – это иногда полезное исключение и даёт чуть больше дать информации в screen reader.
Пример стилизации visually hidden:
export const createVisuallyHiddenComonent = (tag: keyof JSX.IntrinsicElements) => styled(tag)`
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
`
Используя эту механику, мы добавили компонент Amount, читающий без пробелов форматирования сумму и код валюты, его локализацию и не ломающийся от настроек screen reader. Данный компонент реализует нечитаемые символы и цифры, предлагая пользователю скрытые от глаз альтернативы.
А если мы обыденно берем в backlog такие задачи, то почему бы нам не открыть на чтение то, что мы скрывали ранее. Помните, я говорил, что выпадающий календарь в DatePicker был скрыт? Теперь он, например, по стрелочкам позволяет посмотреть конкретный день недели в месяце. А Slider был открыт для screen reader за 15 минут реализации задачи. Отныне у нас заблокировано внедрение компонентов, содержащих aria-hidden, если того не требует конкретное бизнес-требование.
Но, к сожалению, так сильно разогнавшись, мы упёрлись в ограничения технологий нашего времени. Tooltip, всегда считавшийся доступным, конкретизировал свою спецификацию доступности, что сделало его неисправным в нашей реализации, а другие сценарии, реализованные согласно самому свежему W3C, не поддерживаются определенными версиями NVDA и JAWS. Пожалуй, на этом этапе я готов признать, что мы столкнулись с новым для нас термином – «кроссридерность». Как жить с данным словом нам ещё предстоит решить, развивая нашу a11y культуру.
И бонусом, холивары на тему опорных точек (data-атрибуты, за которые можно цепляться при тестировании) и селекторов для автотестирования привели к использованию именно accessibility-атрибутов, как самых логичных и стабильных вспомогательных узлов в структуре HTML-документа.
Самая частая команда в наших автотестах:
cy.findByRole('heading', { name: 'Шаг 2 Workflow' })
cy.checkA11y()
Доступность как религия
В завершение я хочу сказать, что нашей команде удалось хоть и с огрехами, но добраться до пика доступности среди наших продуктов. Мы не везде так хороши, как можем себе казаться, но очень горячо принимаем в работу предложения по улучшению, поскольку мы начали лучше понимать нашего клиента, ведь он находится среди нас.
Культ доступности развивается и растёт, мы сформировали справочный курс для новичков, погружающий их в новый для многих мир (до сих пор на собеседованиях я часто слышу, что доступность – что-то, что кушает семантику, но не более того). Из своего опыта мы знаем, что доступность крайне важна на любых ресурсах, ведь не угадаешь заранее, кто может начать пользоваться твоим ресурсом уже завтра.