Примерно так и получилось с тем «как должны работать диалоги», точнее «правильные» с точки зрения 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 — и половина проблем будет решена
Демо React-focus-lock — codesandbox.io/s/jvl0k6zyk3 (найдите разницу).
Демо Vue-focus-lock — codesandbox.io/s/l5qlpxqvnq
<FocusLock>
<Modal>
any data
</Modal>
</FocusLock>
Но только половина, так как aria-hidden (или inert) вешать прийдется кому-то другому и куда-то в другое место. Но это уже другая история.
Итого
Итого не забывай %username%, что модалы — это не только серенький лайтбокс, что не прокликивается мышкой, но и дивный мир клавиатурных упражнений.
Но самое главное — не забывай что не надо мешать пользователю оперировать с сайтом не только мышкой.
PS: А еще лучше включить VoiceOver или другой ScreenReader и попробуйте свои сайты на прочность. Будете удивлены.
Многие вещи, например «ручная клавиатурная навигация» в ЯндексПочте — дефакто не не меняет активный элемент.
Одного програмиста из Финляндии Яндекс точно потерял как пользователя.
PPS: Gmail, правда, не так чтобы сильно лучше.
Комментарии (23)
saggid
19.09.2017 21:42-2Никогда в жизни даже не думал о каких-то там табах при открытых модалах… И честно говоря не хочу думать, мы же не тюрьму проектируем, из которой надо обязательно запретить убежать любому попавшему в неё..
SirEdvin
19.09.2017 22:00+1Но вот проблема — у модала обычно дефолтная кнопка "Отмена", а перейти на другую кнопку я могу только мышкой, хотя было бы удобно нажать Tab, а затем Enter.
saggid
19.09.2017 22:00-2В крайнем случае, гурманы клавиатурного подхода (каким являюсь в том числе и я сам) всегда имеют возможность увеличить свои возможности с помощью браузерных расширений. Я, к примеру, активно использую Vimium. Прекрасная вещь для тех, кто хочет нажимать на нужные элементы чисто с помощью клавиатуры.
Для всех остальных 98% населения этой планеты вообще будет по барабану, уходит ли ваш фокус куда-то не туда, или не уходит) Сидеть и тратить своё рабочее время на эти 2%, которые свою же проблему могут всё равно без особых проблем решить с помощью браузерных расширений (благо у них и мозгов на это обычно хватает), — я лично не вижу смысла. Лучше потратить это время на другие важные для проекта дела.
SirEdvin
19.09.2017 22:03+1Мне нужен клавиатурный подход, мне нужна возможность быстро закрыть модальное окно.
Понятно, если вы делаете сайт для домохозяек, то да — им все равно. А если вы делаете сайт для достаточно продвинутых пользователей интернета, которые привыкли часть задач выполнять клавиатурой, потому что это в 1000 раз быстрее, то такие штуки портят впечатление от сайта.
Отдельный привет всем тем формам ввода, которые не дают мне возможность табать код на сайтах для программистов.
saggid
19.09.2017 22:11Вообще, для закрытия модального окна обычно достаточно повесить реакцию на нажатие кнопки Escape :) Кстати, название символическое прямо, соответствующее контексту этой беседы)
SirEdvin
19.09.2017 22:12Escape вроде обычно тоже работает как отмена, разве нет?
saggid
19.09.2017 22:13Команда отмены чем-то отличается от закрытия модала?
SirEdvin
19.09.2017 22:16Иногда модалы бывают двух-кнопочные, я описывал такой случай.
saggid
19.09.2017 22:18Да, в целом я понимаю вашу логику и желания. Просто на мой взгляд, это совсем не критично) Очень малое кол-во людей будет пользоваться этим, и многие из них всё равно уже пользуются браузерными расширениями для облегчения работы с клавиатуры. Советую и вам кстати тоже попробовать, возможно тоже войдёте во вкус)
bohdan4ik
19.09.2017 22:55Прочёл дальше этот тред.
Вы немного ошибаетесь, поскольку в статье речь идёт о доступности (это когда человек, к примеру, не видит и пользуется скрин ридером, соответственно, остаётся только клавиатура в его распоряжении), а не о запрете выхода из модалки без тыканья в кнопку табом.saggid
20.09.2017 08:40Ну, это конечно да. Если затачивать сайт под таких людей, это уже становится нужным и важным.
nohuhu
20.09.2017 01:50+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. :)
kashey Автор
20.09.2017 03:42Вы немного не последовательны:
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 в полной красе.nohuhu
20.09.2017 06:10п. 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.kashey Автор
20.09.2017 06:53Ну насчет aria-hidden – лично меня спецификация не очень убедила. Точнее он все еще более правильный чем aria-busy, тем более достаточно часто контент за пределами модала и не виден пользователю (или не читабелен).
Пока самое хорошее решение — или inert или blockingElements. Но ни того, ни другого в браузеры не завезли.
С фокусом проблем нет — банально проверено на читалках. Но вообще тест очень простой — после ловушки на очень большом растоянии располагалается еще один фокусируемый элемент. Если фокус перескочет на него — произойдет скрол страницы. Но если вызвать preventDefault — то скрола не будет.
Другая проблема что порядок таббания отличается от перехода по элементам стрелками(VioceOver), что (конечно же) может все сломать и с этим уже бороться бесполезно.
А вот проблема с tabIndex раскиданным по странице с точки зрения порядка обхода — и не проблема вовсе — github.com/theKashey/focus-lock/blob/master/src/focusMerge.jsnohuhu
20.09.2017 20:11Ну насчет aria-hidden – лично меня спецификация не очень убедила. Точнее он все еще более правильный чем aria-busy, тем более достаточно часто контент за пределами модала и не виден пользователю (или не читабелен).
Я согласен с тем, что данный момент в спецификации не очень хорошо освещён, поэтому возможны различные интерпретации. Вариант, которым я с вами поделился, был рекомендован специалистами по доступности из University of Washington — это люди, которые за большие деньги консультируют компании навроде Microsoft и Amazon. Как минимум один из этих экспертов сам незряч и постоянно пользуется экранными читалками. Я склонен принимать их рекомендации к руководству, а вы решайте сами.
С фокусом проблем нет — банально проверено на читалках.
Вы проверяли на всех читалках, включая мобильные? На всех возможных настройках? Они очень разные бывают.
Но вообще тест очень простой — после ловушки на очень большом растоянии располагалается еще один фокусируемый элемент. Если фокус перескочет на него — произойдет скрол страницы. Но если вызвать preventDefault — то скрола не будет.
Поясните пожалуйста, каким образом прокрутка документа влияет на порядок анонсирования экранных читалок. Насколько я знаю, это вещи не связанные.
Я понимаю и разделяю ваше желание обойтись малой кровью, поэтому и делюсь проверенными на практике решениями. В случае с модальным диалогом все известные мне проблемы снимаются ловушками фокуса и модальной маской, как описано в первом сообщении. Я просто не стал цитировать все тикеты от клиентов, которые привели к этому результату. :)
Другая проблема что порядок таббания отличается от перехода по элементам стрелками(VioceOver), что (конечно же) может все сломать и с этим уже бороться бесполезно.
Если таббабельные элементы находятся только внутри модального диалога, то слишком уж больших проблем возникнуть не должно при любом разумном раскладе. Вообще
tabIndex
> 0 это зло, которого надо избегать.
А вот проблема с tabIndex раскиданным по странице с точки зрения порядка обхода — и не проблема вовсе
Это вам сейчас так кажется. А потом начинают лезть специальные случаи, и батарея проверок вырастает в два метра длиной. Не спрашивайте, откуда я всё это знаю. :)
kashey Автор
21.09.2017 00:34Чувствуется опыт, друг ошибок трудных.
С aria-hidden/busy, насколько я понимаю, история уровня zoom:1 – в общем мы методом тыка проверили как лучше, и лучше вот так. За стандарт немного обидно, но в вебе так везде и всегда.nohuhu
21.09.2017 03:42Чувствуется опыт, друг ошибок трудных.
Он самый.
С aria-hidden/busy, насколько я понимаю, история уровня zoom:1 – в общем мы методом тыка проверили как лучше, и лучше вот так. За стандарт немного обидно, но в вебе так везде и всегда.
Стандарт WAI-ARIA это живой документ, он не далеко идеален, но работа по улучшению идёт постоянно. В версии 1.1 добавили уже много такого, чего страшно не хватало раньше, особенно для работы с табличными виджетами. Надеюсь, что браузеры скоро начнут эти нововведения поддерживать, и жизнь должна облегчиться очень заметно.
noodles
20.09.2017 21:34Большое спасибо за подробное разъяснение по ловушкам. Буквально сегодня делал кастомный попап, и сразу же воспользовался всеми вашими советами. Работает превосходно.
nohuhu
20.09.2017 22:15Рад, что помог. Выше давал ссылку на демо-приложение на базе фреймворка Ext JS, в котором я разрабатывал в т.ч. поддержку доступности, рекомендую заимствовать идеи по полной программе. :) Шишек пришлось набить изрядно, вам этот опыт повторять не обязательно.
denis_invader
20.09.2017 03:27Но у вас Shift+Tab не зациклен, как Tab, а всегда упирается в первый элемент
kashey Автор
20.09.2017 03:31Спасибо. Совсем забыл про эту «фичу», когда внутри focus-lock находяться первый элемент с tabIndex=1, который оказывается самым-самым первым на странице.
В общем случае с самого первого или самого последнего можно выйти во внешний мир и/или просто начать не правильно работать.
Пофиксил react версию focus-lock просто добавим элемент с tabIndex=1 за пределами ловушки, и обновил ссылки на примеры в статье — теперь работают секси.
PS: Я вообще не совсем уверен откуда я взял старую ссылку — она использует старую версию библиотек. В общем спасибо за комментарий.
Riim
Shift+Tab как-то странно работает, фокус либо переходит вперёд, как без Shift, либо (дойдя до последнего варианта) моргает и остаётся на том же месте.