Люди которые пишут стандарты — очень хитро устроились. Им достаточно написать как должно все хорошо работать, а дальше уже не их проблемы.
Примерно так и получилось с тем «как должны работать диалоги», точнее «правильные» с точки зрения a11y модальные диалоги.
В описание к dialog role на MDN все написано очень просто:
- The dialog must be properly labeled
- Keyboard focus must be managed correctly
Проблема в том, что MDN забыла еще об одном важном пункте, а все остальные забыли про один из сказанных – про то, что модал не должен выпускать фокус из своих рук. Активный элемент надо посадить под замок. Не дать ему сбежать из нашей ловушки.
Modal dialog
История началась совсем недавно — в рассылке Веб-стандартов попалась мне ссылка на «правильный» WAI-ARIA Dialog. И понеслось.
Компонент на самом деле хорош:
- он вешает aria-hidden на страницу, чтобы скрыть контент от screen-readers (работает только в первом примере).
- он затеняет контент и вырубает скрол странице.
- контролирует фокус, так чтобы из модала нельзя было табнуться.
- после закрытия диалога он возвращает фокус на исходную позицию.
- и добавляет разные aria-специфичные тэги, конечно же.
Те он делает все что просит MDN и даже больше, так как без первого пункта «выйти» из диалога с активированным screen reader — не составляет никакого труда.
В общем — must have!
Focus
Но вот реализация "focus-management" немного подкачала — ребята реально перехватывают keyboard events(и не только) и эмулируют кнопку tab самостоятельно.
Настолько уму не постижимо, что я решил немного покопать как «это» вообще должно работать. Под словом «покопать» подразумевается проверить как различные сайты и фреймворки справляются с табами.
Начнем с сайтов (немного предвзятая выборка):
- Google Gmail|G+ — идеально. ️
- Одноклассники — с уходом таба закрывают модал, на который фокус так и не приходит
- FB — зависит от страницы. В группе/на личной страницы — ничего нет, в момент написания сообщений есть. Никогда не жмакайте Таб(в сафари) на главной — крышу сносит.
- VK — страница «рандомно» игнорирует таб ️
- Yandex.Maps — страница полностью игнорирует таб ️
- Yandex.Music — страница полностью игнорирует таб ️
- РСЯ — нет focus management.
- LiveJournal — нет focus management.
- Мои собственные сайты — нет focus management.
- Habrahabr — нет ни focus management, модалов
- Jira/Confluence — идеально. ️
Вывод простой — у «нормальных» сайтов немного не хватает мозгов, а Яндексу руки оторвать.
С фреймворками (немного предвзятая выборка) сильно интереснее:
- jQuery UI — по focusIn за пределами модала вешает фокус обратно в определенном порядке. Есть селектор для tabbable и focusable. Работает на честном слове, но хорошо — github.com/jquery/jquery-ui/blob/master/ui/widgets/dialog.js#L300
- Ant.Design — на самом деле использует rc-dialog для этого дела, который вешает хэндер только на Tab. Через shift+tab можно выйти обратно.
И вообще это самый кривой код из всех тут представленных, даже ссылку давать неприятно — github.com/react-component/dialog/blob/master/src/Dialog.tsx#L133
- BluePrint.js — умеет ОЧЕНЬ хорошо настраиваемый Модал, в том числе есть свойство enforceFocus для этого дела. Одно плохо — работает только для autoFocus полей, и полей с заданным tabIndex. О чем они думали? — github.com/palantir/blueprint/blob/master/packages/core/src/components/overlay/overlay.tsx#L312
- Bootstrap? Кидает фокус сам на себя. Shift+Tab упирается в начало и застревает — github.com/twbs/bootstrap/blob/900da3e235305c2daefe86c0a960e36be6e1b60b/js/src/modal.js#L280
- AUI — единственный фреймворк, который имеет выделенный класс для focus-manger и вообще работает «правильно» — по focusOut и event.relatedTaget — bitbucket.org/atlassian/aui/src/92b8ce839ef1b6f320fe6a590def1cbc40cd2724/src/js/aui/focus-manager.js
- Atlaskit, RamblerUI, MaterialUI и даже Semantic-UI(+React версия) — никаких намеков на focus management.
С фреймворками оказывается тоже совсем плохо. И совсем никто-никто не вешает aria-hidden на остальной контент, чтобы сделать модал на самом деле доступным для людей, которые вынуждены использовать скрин-ридеры.
Офтопик
На самом деле я тоже раньше совершенно не заморачивался всей этой фигней, но тут, как на зло, моя дорогая жена решила научиться верстке в HTML Academy где pepelsbey с большой-большой компанией заставили и ее и меня задуматься над вопросом насколько таббабельный у меня сайт.
Пришлось и науку новую выучить, и проблемму решить с фокусом.
PS: Вадим рекомендует забить на всю эту aria-hidden с focus-management и воспользоваться html атрибутом inert, который просто «выключит»(врям совсем) все кроме модала и проблем нет будет ни с screen/reader, ни с фокусом.
Хотя насчет второго не уверен, да и работает он пока не очень, а полифилы просто ужасающие.....
Focus Lock
В общем, как говорили на улице Льва Толстого… – а какие же ваши предложения.
На самом деле проблема очень проста — не смотря на то, что для JS было написано миллионы модулей — модулей для focus management фактически нет.
- focus-manager — простой focus-manager с простым и ванильным API и отличным примером. Есть пара минусов
- ToleFocus — какой-то монстр, от которого бежать хочется.
- react-focus-trap — настолько простой, что возвращает фокус только в начало.
- Focus manager из AUI, но кто раньше слышал про AUI?
- focus-trap, он же focus-trap-react который был использован в WAI-ARIA демке в начале статьи. И который по дефолту выключается по Esc и вообще не очень правильно использует DOM-API
В общем 7 бед = +1 новый велосипед. А точнее настоящий поезд из focus-lock, dom-focus-lock, react-focus-lock и vue-focus-lock — на все случаи жизни.
Со стороны обертки (react, vue, dom) все очень просто — получить DOM ноду и закрыть в ней фокус. Вся соль именно в focus-lock.
Причин создания новой библиотеки несколько:
- К сожалению все решения(кроме focus-trap/lock) совершенно полностью игнорируют tabIndex и становятся полностью неработоспособными если один умный програмист сломает порядок таббания.
Случай, конечно, немного синтетический, но вполне реальный. К моему большому большому сожалению. - Из всех решений (кроме focus-trap/lock и react-focus-trap) можно без проблем табнуться в сафари(JFYI: сафари различает Tab и Opt+Tab). И если фокус единожды покинет ловушку — назад его уже никто не вернет.
- focus-trap, который так хорошо везде работает, делает это потому что перехватывает и эмулирует Tab, те полностью игнорирует настройки того же Safari пунктом выше.
- Все решения(кроме focus-lock и BluePrint.js) по входу селектят первый элемент, а не элемент с автофокусом.
PS: focus-trap ищет элемент с атрибутом initialFocus. С чего бы?
Так что пришлось сделать очередной велосипед, который временно отвечает чуть большему списку свистелок, чем его ближайшие конкуренты. Или конкретно всем.
Просто оберни модалы(и не только модалы) в FocusLock — и половина проблем будет решена (демо).
<FocusLock>
<Modal>
any data
</Modal>
</FocusLock>
Но только половина, так как aria-hidden (или inert) вешать прийдется кому-то другому и куда-то в другое место. Но это уже другая история.
Итого
Итого не забывай %username%, что модалы — это не только серенький лайтбокс, что не прокликивается мышкой, но и дивный мир клавиатурных упражнений.
Но самое главное — не забывай что не надо мешать пользователю оперировать с сайтом не только мышкой.
PS: А еще лучше включить VoiceOver или другой ScreenReader и попробуйте свои сайты на прочность. Будете удивлены.
Многие вещи, например «ручная клавиатурная навигация» в ЯндексПочте — дефакто не не меняет активный элемент.
Одного програмиста из Финляндии Яндекс точно потерял как пользователя.PPS: Gmail, правда, не так чтобы сильно лучше.
Автор: kashey