
На связи Даниил Высоцкий, неравнодушный эксперт разработки с проекта единой платформы государственных услуг, и Вера Шингарёва, экс-специалист по доступности интерфейсов компании РТЛабс.
Про работу над доступностью на Госуслугах
Госуслуги — это один из самых высоконагруженных ресурсов в Рунете, с ежедневной базой пользователей более 11 млн человек.
Наша небольшая команда помогает делать доступными сервисы Госуслуг на сайтах и в мобильных приложениях. Мы проверяем интерфейсы на соответствие требованиям доступности, работу с клавиатуры и с помощью программ экранного доступа. Находим проблемы и передаём их в разработку, консультируем разработчиков по вопросам доступной вёрстки.
При проверке и составлении требований опираемся на базу WCAG — документацию по веб-доступности, и ARIA Patterns — лучшие практики по доступным интерфейсам. А для отладки используем Chrome DevTools — разделы Elements и Accessibility. Одна из программ экранного доступа, которые мы используем для проверки веб-сервисов — NVDA.
Почему именно NVDA? Во-первых, это самый популярный скринридер в России.
Статистика использования скринридеров в России среди людей с нарушениями зрения по данным Яндекса за 2023 год:

Во-вторых, это бесплатный, многоязычный проект с открытым исходным кодом, который постоянно развивается и учитывает современные веб-стандарты. К тому же разработчики NVDA знают «боли» своих пользователей не понаслышке — более 12 тысяч закрытых проблем на гитхабе. Немногие проекты могут похвастаться такими цифрами.
Единственный минус — NVDA работает только на Windows. Но по данным от того же Яндекса, Windows установлен у большинства пользователей — 98,7%.
Статистика использования операционных систем среди людей с нарушениями зрения в России по данным Яндекса за 2023 год:

Для MacOS и Linux есть мультиплатформенные и штатные альтернативы, но, поскольку самый популярный скринридер в России — NVDA, мы в команде доступности используем именно его.
Фраза «по щелчку»
Однажды, проверяя формы на Госуслугах с помощью NVDA, мы столкнулись со странной проблемой: при фокусировке на текстовых элементах и блоках скринридер озвучивал фразу «по щелчку». При этом ни одной ссылки или кнопки внутри текста, которая могла бы объяснить такое поведение скринридера, не было. Для наглядности сделали видео, где скринридер проходит по тексту в услуге и кнопке, озвучивая на них «по щелчку».
Вроде бы текстовый блок без ссылок, а озвучивается «по щелчку»:

Для большей убедительности показываем html-код — ссылок нет, обычная семантическая вёрстка:

Фраза «по щелчку» на кнопке в услуге избыточна, потому что уже сказано «кнопка» — значит, по ней можно кликнуть:

Ещё один текстовый блок без ссылок с озвучкой «по щелчку»:

Почему это проблема
На первый взгляд, может показаться, что это и не проблема — просто одно лишнее слово. Но мы спросили у незрячих пользователей, что они думают, когда слышат фразу «по щелчку» при использовании скринридера NVDA:
«О, да. Я не люблю, когда такое приходит в уши, потому что не понимаю, что это значит, и что с этим делать. У меня это со щелбаном по носу ассоциируется 🙂 Абсолютно лишняя для меня аудиоинформация».
— Илья Лебедев, незрячий пользователь
«Бесит, только когда в каком-нибудь приложении, ни у одного из элементов ролей нет вообще (все див-контейнеры), всё просто стилизировано CSS и везде висят обработчики и у тебя весь сайт по щелчку. Бесит не фраза, а то, что ты типа элемента не можешь определить, то ли это кнопка, то ли это ссылка, то ли это раскрывающийся список и так далее».
— Кирилл Шмелёв, системный аналитик
«Я всегда убираю, мне ��та фраза мешает. Иногда он [скринридер] говорил на одном элементе по щелчку много раз, типа по щелчку, по щелчку, по щелчку, по щелчку название кнопки. Теперь это можно настраивать и выключать, раньше нельзя было».
— Алексей Самойлов, тестировщик невизуальной доступности и программист
Вырисовывается несколько неприятных пользовательских сценариев:
пользователь не может определить, что за элемент перед ним и как с ним взаимодействовать
при озвучке «по щелчку» у текстового блока, пользователь предполагает, что с ним можно взаимодействовать, и начинает безрезультатно «кликать» по блоку. Либо «рыскать» по содержимому блока в поисках кликабельных элементов, не находя их, потому что они не всегда присутствуют
пользователь отключает озвучку интерактивных элементов. Таким образом, он ухудшает доступность сам себе — не всегда может узнать, что здесь вообще есть интерактивные элементы.
В результате это создаёт путаницу и сбивает пользователя, мешает ему выполнить свою задачу в интерфейсе.
Как отключить в скринридере
Оказывается, озвучивание этой фразы можно отключить: настройки NVDA → форматирование документа → интерактивные элементы. Обычно пользователи не занимаются такой кастомизацией настроек скринридера.
Фраза «по щелчку» озвучивается, когда стоит галочка «Интерактивные элементы»:

Этот чекбокс влияет только на слово «по щелчку» — отключает озвучку интерактивных элементов без типа, сделанных на дивах и спанах. И не затрагивает элементы с заданной ролью кнопки, ссылки, списка и так далее — они будут озвучиваться как положено.
Ищем корень проблемы
Фраза «по щелчку» подразумевает, что по этому элементу можно кликнуть, а значит, на нём висит обработчик событий.
И действительно, на проблемных компонентах из примеров выше были добавлены обработчики кликов, которые можно увидеть на вкладке Event Listeners.
Обработчик кликов для пояснения lib-explanation:

Кроме того, во вкладке Accessibility таких элементов отображалась роль generic. У проблемных компонентов отображается role: generic:

Вот что говорит Дока — документация для разработчиков, про эту роль:
«role="generic" означает, что у элемента нет семантики и имени.
В HTML роль generic есть у <div> и <span>».
Хотя мы видим или слышим, что какая-то семантика есть, а у обычных дивов и спанов нет ролей. Взяли рандомный див, у которого не было никаких ролей и других Computed Properties:

Копаем глубже
Мы нашли issue в репозитории NVDA, описывающую эту проблему. Оказывается, баг с озвучкой «по щелчку» встречается только в NVDA, в JAWS такой проблемы нет.
Проблема возникает на неинтерактивных элементах <div>, <span>, <p>, кастомные, если на них добавлен обработчик событий click, mouseDown или mouseUp.
На интерактивных элементах <button>, <input>, <select>, <a> и других такого не происходит. Потому что для них озвучивается роль, которая уже подразумевает взаимодействие: кнопка, редактор, комбинированный список, ссылка.
В наших компонентах часто используются контейнерные обёртки, на которые вешаются события для делегирования. Из-за этого фраза «по щелчку» появляется даже на кнопках «button».
Что касается текстовых блоков, то в них могут быть ссылки, поэтому сразу на весь блок вешается обработчик событий.
Как мы видим на вкладке Event Listeners, у контейнера для текста со ссылками стоит обработчик кликов:

Как убрать «по щелчку»
Чтобы избавиться от лишней озвучки, нужно добавить таким элементам атрибут role="presentation" — эта роль удаляет семантику элемента. У роли presentation есть синоним none, но использовать его нельзя, потому что пока у role="none" плохая поддержка. Роль presentation удаляет только семантику блока, на котором находится, и не затрагивает дочерние элементы. Так что можно не опасаться, что наша кнопка превратится в кирпич, и смело добавлять role="presentation" на блок.
Добавили role="presentation" на контейнер кнопки:

Теперь «по щелчку» не озвучивается на кнопке:

Добавили role="presentation" на контейнер пояснения lib-explanation:

Теперь «по щелчку» не озвучивается на пояснении:

Проблема глазами разработчика
На Госуслугах используется фреймворк Angular 2+, в котором есть много инструментов для решения различных технических задач. Конкретно с багом «по щелчку» проглядывались следующие трудности:
не было понятно, какое количество компонентов затронуто багом «по щелчку»
не было единого критерия, позволяющего определить, какие компоненты точно «заражены» проблемой, а какие потенциально могут оказаться в списке
нужно было создать единый механизм или решение, которое легко можно было бы имплементировать в компоненты с дефектным поведением.
Первые две проблемы были решены сравнительно легко: найденный issue в репозитории NVDA чётко определил критерий «дефекта» — любой обработчик кликов addEventListener('click', ...) является причиной проблемы. То есть достаточно было бы посмотреть, сколько раз в проекте встречается конструкция создания обработчика кликов, чтобы можно было понять и оценить масштаб бедствия.
В Angular вместо addEventListener используется свой отдельный декоратор @HostListener на уровне класса компонента, который позволяет навешивать обработчики на DOM-события. Например, так может выглядеть обработка клика по ссылке, которая ведёт в личный кабинет в обход штатных механизмов открытия странички по ссылке:
@HostListener('click', ['$event']) onClick($event: Event): void { const { id } = $event.target as HTMLElement; if (id === 'linkToLK') { $event.preventDefault(); this.redirectToLK(); } }
Поиск по всему проекту подстроки @HostListener('click' выдал всего 6 файлов, в которых использовались обработчики кликов. Один из таких файлов была самописная Angular-директива ClickableLabelDirective, которая обрабатывала клики по нестандартным ссылкам с дополнительными data-атрибутами.
Поиск по проекту селектора этой директивы показал, что её используют 14 компонентов. В общем итоге получилось, что нужно поддержать исправление на уровне этой директивы, чтобы разом починить 14 компонентов и точечно проверить 5 вхождений с явным использованием @HostListener('click' — целевой ли в них сценарий использования, то есть применяется ли он к интерактивным элементам из списка выше.
Дело оставалось за малым — ответить на вопрос: как обеспечить проброс необходимого role="presentation" каждому компоненту, который использует директиву ClickableLabelDirective. Решение родилось само собой, как только вспомнили, что в Angular существует класс Renderer2, который позволяет управлять атрибутами элементов DOM-дерева. Оставалось написать заветные 7 строчек кода внутри директивы ClickableLabelDirective:
public ngOnInit(): void { this.renderer.setAttribute( this.elementRef.nativeElement, 'role' 'presentation' ); }
Где ngOnInit() — один из первых лайф-хуков Angular компонента или директивы, а this.elementRef — ссылка на сам элемент.
Получается, любой компонент, который использует или будет использовать директиву ClickableLabelDirective, автоматически обогащается атрибутом role="presentation", что нам и требовалось. Теперь любой текстовый блок не будет сопровождаться паразитным «по щелчку» в NVDA. Но при этом фокус на возможной внутренней ссылке в рамках блока будет озвучиваться по всем стандартам доступного интерфейса.
Вывод

Как показывает практика, писать семантически корректную вёрстку недостаточно для обеспечения хорошего accessibility experience (AX). Требуется также внимательное тестирование теми техническими средствами, которые пользуются популярностью у конечных пользователей. Нужно также выявлять и исправлять подобные вендор-специфичные баги, которые существенно усложняют пользовательский опыт, если не обращать на них внимание. И не забывать заводить issue в бэклоги опенсорсных проектов — осведомлённость разработчиков повышается, а ваша карма улучшается. 😊
