[Из песочницы] Улучшение UX при работе с клавишей «Tab»

При разработке приложений «фронтендеры» редко обращают внимание на то, как пользователь будет использовать предоставляемые браузером функции клавиш. Я не являюсь исключением, но в один день мне была дана задача касаемо UX и переходов с помощью нажатия «Tab» и «Shift + Tab».

Суть задачи прозрачна и чиста: есть интерфейс, макет которого отображен ниже. Концептуально 1 страница может содержать 2 различные формы и требованием было условие, чтобы «бегание «Tab`ами» не переходило с 1 формы на другую».

image

Все было бы хорошо, если бы браузер умел «нативно» блокировать фокус в формах. Пример представлен на рисунке ниже, где оранжевым «border`ом» помечен текущий элемент, а серым — предыдущий.

image

Как видите, «нативное» поведение не удовлетворяет требованиям. Итак, давайте решим эту проблему. Решение не является сложным, так что рассмотрим его.

Было бы идеально если бы существовали некие «ворота», которые не допускали бы «выпрыгивание» фокуса из последнего (при «Tab») или первого (при «Shift + Tab») элемента с «tabindex» или поддерживающего фокус по-умолчанию. Итак, суть проста: наши «ворота» — это спрятанные «input-элементы», которые при событии «onFocus» получают событие «event» в виде аргумента и возвращают фокус на элемент, с которого он пришел. Иллюстрация ниже.

image

Получение предыдущего элемента осуществимо с использованием свойства «relatedTarget» объекта «event». Визуализация решения ниже.

image

А вот и сам код. Стоит отметить, что здесь отсутствует «ES6+» синтаксис, так как в основе лежит идея поддержки кода различными браузерами без подключения библиотек типа «Babel».

function getGateInput(handleTabOut) {
    var input = document.createElement("input");
    // not visibiliy:hidden or display:none as need to focus on this element
    var hiddingStyle =
      "opacity: 0;cursor: none;position: absolute;top: -10px;left: -10px;";

    input.setAttribute("style", hiddingStyle);
    input.addEventListener("focus", handleTabOut);

    return input;
  }


Здесь нет ничего сложного: создается «input», устанавливаются стили, которые «спрячут» наши «ворота». Здесь не используется «display: none», так как браузер не фокусирует «Tab`ами» такие элементы. Вследствие такого поведения требуется сделать элемент прозрачным и вынести за пределы окна браузера.

function getTabOutHandler(element, GATES) {
    return function(event) {
      var relatedTarget = event.relatedTarget || event.fromElement;
      var target = event.target;
      var gatesTrapped = target === GATES[0] || target === GATES[1];

      if (gatesTrapped && isChild(relatedTarget, element)) {
        event.preventDefault();
        relatedTarget.focus();
      }
    };
  }


Для возврата фокуса на предыдущий элемент используется getTabOutHandler. Это «HOC». Первым его аргументом является наш контейнер (то, вокруг чего мы устанавливаем «ворота»), а вторым он ожидает массив «ворот», которые мы создали с помощью getGateInput. Данная функция возвращает обработчик события, который работает по описанному выше принципу.

Для того, чтобы фокус мог зайти в контейнер нам надо открывать и закрывать «ворота». Это мы будем делать путем установки атрибута «tabindex». (-1 — не фокусировать «Tab`ами», 0 — фокусировать согласно потоку)

function moveGates(open, GATES) {
    GATES[0].setAttribute("tabindex", open ? -1 : 0);
    GATES[1].setAttribute("tabindex", open ? -1 : 0);
  }


Для управления воротами установим обработчик, который будет «слушать» нажатие «Tab`а»(код 9) и если сфокусированный элемент (activeElement) находится внутри контейнера, то закрыть «ворота», иначе — открыть.

window.addEventListener("keydown", function(event) {
      if (event.keyCode === 9) {
        if (isChild(document.activeElement, element)) {
          moveGates(false, GATES);
        } else {
          moveGates(true, GATES);
        }
      }
    });


Итого


Был рассмотрен способ блокировки фокуса в форме, который заключается в возврате фокуса на предыдущий сфокусированный элемент. Для «отлавливания» фокуса мы использовали скрытые «input-элементы», фокусировка которых регулировалась с помощью «tabindex`а». Представленный выше код является частью библиотеки «tab-out-catcher», который я написал для решения своей задачи. Примеры использования можно посмотреть здесь. Так же есть решение для React приложений.

© Habrahabr.ru