JavaScript
HTML
Comments 24
0

Shift+Tab как-то странно работает, фокус либо переходит вперёд, как без Shift, либо (дойдя до последнего варианта) моргает и остаётся на том же месте.

-2

Никогда в жизни даже не думал о каких-то там табах при открытых модалах… И честно говоря не хочу думать, мы же не тюрьму проектируем, из которой надо обязательно запретить убежать любому попавшему в неё..

+1

Но вот проблема — у модала обычно дефолтная кнопка "Отмена", а перейти на другую кнопку я могу только мышкой, хотя было бы удобно нажать Tab, а затем Enter.

-2

В крайнем случае, гурманы клавиатурного подхода (каким являюсь в том числе и я сам) всегда имеют возможность увеличить свои возможности с помощью браузерных расширений. Я, к примеру, активно использую Vimium. Прекрасная вещь для тех, кто хочет нажимать на нужные элементы чисто с помощью клавиатуры.


Для всех остальных 98% населения этой планеты вообще будет по барабану, уходит ли ваш фокус куда-то не туда, или не уходит) Сидеть и тратить своё рабочее время на эти 2%, которые свою же проблему могут всё равно без особых проблем решить с помощью браузерных расширений (благо у них и мозгов на это обычно хватает), — я лично не вижу смысла. Лучше потратить это время на другие важные для проекта дела.

+1

Мне нужен клавиатурный подход, мне нужна возможность быстро закрыть модальное окно.


Понятно, если вы делаете сайт для домохозяек, то да — им все равно. А если вы делаете сайт для достаточно продвинутых пользователей интернета, которые привыкли часть задач выполнять клавиатурой, потому что это в 1000 раз быстрее, то такие штуки портят впечатление от сайта.


Отдельный привет всем тем формам ввода, которые не дают мне возможность табать код на сайтах для программистов.

0

Вообще, для закрытия модального окна обычно достаточно повесить реакцию на нажатие кнопки Escape :) Кстати, название символическое прямо, соответствующее контексту этой беседы)

0

Иногда модалы бывают двух-кнопочные, я описывал такой случай.

0

Да, в целом я понимаю вашу логику и желания. Просто на мой взгляд, это совсем не критично) Очень малое кол-во людей будет пользоваться этим, и многие из них всё равно уже пользуются браузерными расширениями для облегчения работы с клавиатуры. Советую и вам кстати тоже попробовать, возможно тоже войдёте во вкус)

0
Прочёл дальше этот тред.

Вы немного ошибаетесь, поскольку в статье речь идёт о доступности (это когда человек, к примеру, не видит и пользуется скрин ридером, соответственно, остаётся только клавиатура в его распоряжении), а не о запрете выхода из модалки без тыканья в кнопку табом.
0

Ну, это конечно да. Если затачивать сайт под таких людей, это уже становится нужным и важным.

+2

Статья несколько сумбурная и трудно читается, плюс смешаны в кучу разные проблемы. Что в общем и понятно, a11y тема большая и сложная. Ради подсматривания идей рекомендую посмотреть на примеры Ext JS, мы по многим граблям уже прошлись. Решения часто не идеальные, но более или менее работают.


Что касается модального диалога, то с ним у меня возникало три проблемы:


  • Как не отпускать фокус из диалога с модальной маской
  • Как не сойти с ума, если фокус всё же убежал под маску
  • Как не дать экранным читалкам сфокусировать элементы под маской

Первая проблема решается относительно просто. Вариантов пользовательского взаимодействия здесь может быть несколько: pointer/touch, клавиатура, прямой доступ к элементам через интерфейс экранных читалок и т.п.


Для pointer/touch можно использовать модальную маску: элемент, визуально находящийся "под" диалогом и закрывающий весь остаток экрана. Все клики/прикосновения вне диалога приземляются на этот элемент и игнорируются.


С клавиатурой есть несколько вариантов, самый надёжный это ловушки: невидимые элементы <span tabindex="0" aria-hidden="true">, размещённые по "краям" диалога. Верхняя ловушка должна располагаться в DOM дереве перед первым таббабельным элементом в диалоге, нижняя, соответсвенно, после последнего. На ловушки вешается обработчик события focus, который определяет, в каком направлении движется фокус, и перебрасывает его на первый таббабельный элемент сверху или последний снизу. Когда пользователь нажимает Tab/Shift-tab, фокус из диалога не сбегает даже в строку URL (это требование WAI-ARIA).


C tabIndex может оказаться не так просто, если внутри диалога есть элементы с tabIndex > 0. Для универсальности перед показом диалога лучше найти все эти элементы, выбрать минимальный tabIndex и присвоить его верхней ловушке, а максимальный, соответственно, нижней.


Что касается прямого фокусирования элементов через экранные читалки, то с этим де факто ничего сделать нельзя. Пользователь может в любой момент вызвать у себя список, скажем, заголовков на странице, таблиц и строк, etc, и перейти напрямую к любому элементу. С этим нужно просто смириться и проектировать сайт/приложение с учётом такого поведения. Для смягчения проблемы можно давать подсказки экранным читалкам, но это тоже не всегда срабатывает (см. ниже).


Вторая проблема сложнее и распадается на две части: как не дать фокусу попасть "под" маску, и как быть, если он там всё же оказался.


Первая часть относительно сложна, т.к. пользователь может начать нажимать Tab с тела документа или из строки URL, и первый таббабельный элемент запросто может оказаться "под" маской. Или диалог может открыться без вмешательства пользователя (ошибка соединения с сервером, etc) и текущий сфокусированный элемент окажется под маской, и т.д., вариантов миллион. Для искоренения таких возможностей мы ищем все таббабельные элементы "под" маской и убираем tabIndex/ставим -1, а после снятия маски восстанавливаем всё как было.


Если же каким-то образом фокус попал "под" маску и пользователь нажал Tab/Shift-Tab, то первым таббабельным элементом в документе окажется фокусная ловушка в диалоге, которая должна отработать событие и направить фокус внутрь диалога. Чтобы ловушка отрабатывала максимально гибко, мы проверяем только направление движения фокуса, но не принадлежность элементов — если фокус прилетел извне диалога, то в общем и всё равно.


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


а) Основную разметку страницы заключаем в дополнительный <div> без позиционирования, который не влияет на раскладку и нужен только для группировки элементов
б) Модальные элементы, включая маску и диалог, добавляем в смежный контейнер, не входящий в основной <div> — это важно
в) При закрытии экрана модальной маской к основному контейнеру добавляем атрибут aria-busy="true", который удаляется при снятии маски


Таким образом мы даём экранной читалке понять, что содержимое страницы, кроме диалога, должно быть недоступно и фокусировать его не нужно. Важно, чтобы диалог с маской не попали в контейнер aria-busy, т.к. в этом случае некоторые читалки просто игнорируют атрибут — нельзя сделать недоступным текущий сфокусированный элемент.


С обработкой клавиатурных событий внутри диалога должно быть более или менее просто: Enter эквивалентен нажатию кнопки по умолчанию, Esc приводит к отмене и закрытию диалога. Это если у вас кнопки OK и Отмена, а вот если, скажем, Да/Нет, то тут уже не так однозначно. Или если в диалоге есть виджеты, "съедающие" Enter/Esc — надо не забывать останавливать событие, иначе, скажем, Esc на открытом списке комбо-бокса закроет не только список, но и диалог тоже.


Много нюансов, но не страшно сложно и нет ничего такого, что нельзя легко закрыть юнит-тестами. Модальные диалоги это всё-таки не grids. :)

0
Вы немного не последовательны:
1. Во первых aria-busy придуман не для этого. Основной контент надо именно «прятать», для чего нужен aria-hidden. И это относится исключительно к пункту 3.
2. А вот пункты 2 и 3 различать не надо. Есть два события — focusIn, когда фокус куда-то пришел, и focusOut, когда он откуда-то ушел.
Большинство библиотек агрятся на попытку фокуса покинуть ловушку, те focusOut, где новый элемент будет записан в relatedTarget. При этом ловушку в любом случае можно покинуть уйдя в адресную строку браузера, и на этом все сломается. Выбрался — значит свободен.
Плюс focusOut — его можно(и нужно) вешать на свою ноду.
К сожалению, так как это не очень работает, требуется вешать хэндлер на focusIn глобально на документе. Теперь, когда что-то за пределами ловушки получает фокус — можно будет этот фокус взять и положить обратно.
Ну а в итоге требуется вешать события и туда и туда.

И самое главное — НИЧЕГО кроме операций с фокусом делать не надо. Никакие keyboard/mouse/touch events.

Тоже самое относиться к предложеным фокусным ловушкам по краям основной. Главный поинт в том, что эти ловушки должны быть за пределами, а не внутри. И исключительно чтобы между началом/конца документа и модалом был что-то таббательное.
И в таком случае их вообще можно не включать в состав библиотеки — focus-lock/dom-focus-lock не могут менять верстку.

В общем KISS в полной красе.
0

п. 1. Почитайте внимательнее спецификацию, там отчётливо не рекомендуется использовать aria-hidden для контента, визуально присутствующего на странице. В случае с модальным диалогом контент присутствует, но должен быть недоступен, а для этого лучше подходит aria-busy.


Однозначного решения для этой проблемы я в спецификации не вижу, а вариант с aria-busy был найден в результате совместного поиска со специалистами по доступности веб-приложений из University of Washington.


п. 2. События focusin и focusout, к счастью, уже работают во всех браузерах — но с очень недавних пор, в Firefox они появились только в мартовской версии. Если вам нужно поддерживать предыдущий LTS, то проблемы будут в полный рост. Вряд ли, но просто учитывайте возможность.


В данном случае использовать события focusin и focusout неоптимально по разным причинам. Во-первых, focusin и focusout стреляют не только при перемещении фокуса на другой элемент, но и при фокусировании/расфокусировании окна самого браузера, и не всегда можно определить, что же именно происходит. Во-вторых, спецификация однозначно утверждает, что (выделение моё):


Like non-modal dialogs, modal dialogs contain their tab sequence. That is, Tab and Shift + Tab do not move focus outside the dialog. However, unlike most non-modal dialogs, modal dialogs do not provide means for moving keyboard focus outside the dialog window without closing the dialog.

Ваш вариант предусматривает реакцию на потерю фокуса диалогом, а это совершенно не эквивалентно предотвращению потери фокуса. В частности, экранные читалки могут, и во многих случаях будут, анонсировать сфокусированный элемент вне диалога, а затем ещё раз анонсировать элемент внутри диалога. Слепым пользователям очень трудно ориентироваться в таких условиях.


Но самая большая проблема с таким подходом возникнет, если у вас есть элементы с tabIndex > 0 где-либо вне диалога. В таком случае порядок табуляции может быть крайне неочевидным для пользователя, и при выходе фокуса за пределы диалога вашему коду нужно будет решать, куда вернуть его обратно в диалог. Тут нужно будет либо запоминать состояние на каждый чих, либо пытаться играть в игры с порядком табуляции, что само по себе весьма нетривиальная задача.


На варианте с ловушками я остановился после нескольких лет экспериментов на кошках с клиентскими приложениями, и это единственный вариант, который закрывает все требования и специальные случаи. Реагировать на события pointer*/touch*/key* с таким подходом не обязательно, модальная маска может быть пассивной, предотвращая фокусирование элементов "под" ней просто за счёт своего положения в DOM и более высокого z-index.

0
Ну насчет aria-hidden – лично меня спецификация не очень убедила. Точнее он все еще более правильный чем aria-busy, тем более достаточно часто контент за пределами модала и не виден пользователю (или не читабелен).
Пока самое хорошее решение — или inert или blockingElements. Но ни того, ни другого в браузеры не завезли.

С фокусом проблем нет — банально проверено на читалках. Но вообще тест очень простой — после ловушки на очень большом растоянии располагалается еще один фокусируемый элемент. Если фокус перескочет на него — произойдет скрол страницы. Но если вызвать preventDefault — то скрола не будет.
Другая проблема что порядок таббания отличается от перехода по элементам стрелками(VioceOver), что (конечно же) может все сломать и с этим уже бороться бесполезно.

А вот проблема с tabIndex раскиданным по странице с точки зрения порядка обхода — и не проблема вовсе — github.com/theKashey/focus-lock/blob/master/src/focusMerge.js
0
Ну насчет aria-hidden – лично меня спецификация не очень убедила. Точнее он все еще более правильный чем aria-busy, тем более достаточно часто контент за пределами модала и не виден пользователю (или не читабелен).

Я согласен с тем, что данный момент в спецификации не очень хорошо освещён, поэтому возможны различные интерпретации. Вариант, которым я с вами поделился, был рекомендован специалистами по доступности из University of Washington — это люди, которые за большие деньги консультируют компании навроде Microsoft и Amazon. Как минимум один из этих экспертов сам незряч и постоянно пользуется экранными читалками. Я склонен принимать их рекомендации к руководству, а вы решайте сами.


С фокусом проблем нет — банально проверено на читалках.

Вы проверяли на всех читалках, включая мобильные? На всех возможных настройках? Они очень разные бывают.


Но вообще тест очень простой — после ловушки на очень большом растоянии располагалается еще один фокусируемый элемент. Если фокус перескочет на него — произойдет скрол страницы. Но если вызвать preventDefault — то скрола не будет.

Поясните пожалуйста, каким образом прокрутка документа влияет на порядок анонсирования экранных читалок. Насколько я знаю, это вещи не связанные.


Я понимаю и разделяю ваше желание обойтись малой кровью, поэтому и делюсь проверенными на практике решениями. В случае с модальным диалогом все известные мне проблемы снимаются ловушками фокуса и модальной маской, как описано в первом сообщении. Я просто не стал цитировать все тикеты от клиентов, которые привели к этому результату. :)


Другая проблема что порядок таббания отличается от перехода по элементам стрелками(VioceOver), что (конечно же) может все сломать и с этим уже бороться бесполезно.

Если таббабельные элементы находятся только внутри модального диалога, то слишком уж больших проблем возникнуть не должно при любом разумном раскладе. Вообще tabIndex > 0 это зло, которого надо избегать.


А вот проблема с tabIndex раскиданным по странице с точки зрения порядка обхода — и не проблема вовсе

Это вам сейчас так кажется. А потом начинают лезть специальные случаи, и батарея проверок вырастает в два метра длиной. Не спрашивайте, откуда я всё это знаю. :)

0
Чувствуется опыт, друг ошибок трудных.
С aria-hidden/busy, насколько я понимаю, история уровня zoom:1 – в общем мы методом тыка проверили как лучше, и лучше вот так. За стандарт немного обидно, но в вебе так везде и всегда.
0
Чувствуется опыт, друг ошибок трудных.

Он самый.


С aria-hidden/busy, насколько я понимаю, история уровня zoom:1 – в общем мы методом тыка проверили как лучше, и лучше вот так. За стандарт немного обидно, но в вебе так везде и всегда.

Стандарт WAI-ARIA это живой документ, он не далеко идеален, но работа по улучшению идёт постоянно. В версии 1.1 добавили уже много такого, чего страшно не хватало раньше, особенно для работы с табличными виджетами. Надеюсь, что браузеры скоро начнут эти нововведения поддерживать, и жизнь должна облегчиться очень заметно.

0
Большое спасибо за подробное разъяснение по ловушкам. Буквально сегодня делал кастомный попап, и сразу же воспользовался всеми вашими советами. Работает превосходно.
0

Рад, что помог. Выше давал ссылку на демо-приложение на базе фреймворка Ext JS, в котором я разрабатывал в т.ч. поддержку доступности, рекомендую заимствовать идеи по полной программе. :) Шишек пришлось набить изрядно, вам этот опыт повторять не обязательно.

0
Но у вас Shift+Tab не зациклен, как Tab, а всегда упирается в первый элемент
0
Спасибо. Совсем забыл про эту «фичу», когда внутри focus-lock находяться первый элемент с tabIndex=1, который оказывается самым-самым первым на странице.
В общем случае с самого первого или самого последнего можно выйти во внешний мир и/или просто начать не правильно работать.
Пофиксил react версию focus-lock просто добавим элемент с tabIndex=1 за пределами ловушки, и обновил ссылки на примеры в статье — теперь работают секси.
PS: Я вообще не совсем уверен откуда я взял старую ссылку — она использует старую версию библиотек. В общем спасибо за комментарий.
Only those users with full accounts are able to leave comments., please.