Ограничить фокусировку tabindex частью страницы

Ситуация:

У меня есть веб-страница, которая открывает модальные окна (лайтбоксы), которые содержат формы, в которые пользователь может вводить данные. Пользователи обычно перемещаются с помощью клавиатуры, переходя от одного поля к другому.

Проблема:

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

Вопрос:

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

Единственное, о чем я могу думать, это использовать Javascript для установки tabindex=-1 на всех элементах формы (и других фокусируемых элементах), когда модальное окно открыто, а затем установите tabindex Значения возвращаются к своим предыдущим значениям, когда модальное окно закрыто. Есть ли более простой / лучший способ?

6 ответов

Нет, это единственный способ.

  1. Найти все элементы, которые имеют tabIndex лучше чем -1 и не принадлежат вашему модалу.
  2. Создать массив и заполнить его ссылками на каждый элемент вместе с его оригиналом tabIndex,
  3. Установить каждый элемент tabIndex в -1 поэтому он больше не может получать фокус с клавиатуры.
  4. Когда модальное диалоговое окно закрыто, выполните итерацию по массиву и восстановите исходный tabIndex,

Вот короткая демонстрация:

function isDescendant(ancestor, descendant) {
  do {
    if (descendant === ancestor) return true;
  } while (descendant = descendant.parentNode);
  return false;
}

var tabIndexRestoreFunctions;
var lastFocused;

document.getElementById("btn-show-modal").addEventListener("click", function(e) {
  lastFocused = document.activeElement;
  var modal = document.querySelector(".modal");
  tabIndexRestoreFunctions = Array.prototype
  // get tabable items which aren't children of our modal
  .filter.call(document.all, o => o.tabIndex > -1 && !isDescendant(modal, o))
  // iterate over those items, set the tabIndex to -1, and 
  // return a function to restore tabIndex
  .map(o => {
    var oldTabIndex = o.tabIndex;
    o.tabIndex = -1;
    return () => o.tabIndex = oldTabIndex;
  });
  // show modal
  modal.classList.add("shown");
  // focus modal autofocus
  modal.querySelector("[autofocus]").focus();
});

document.getElementById("btn-close-modal").addEventListener("click", function(e) {
  // restore tabs
  tabIndexRestoreFunctions && tabIndexRestoreFunctions.forEach(f => f());
  tabIndexRestoreFunctions = null;
  // hide modal
  document.querySelector(".modal").classList.remove("shown");
  // restore focus
  lastFocused && lastFocused.focus();
});
.modal {
  display: none;
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(128, 128, 128, .75);
}
.modal.shown {
  display: flex;
}
.modal-content {
  margin: auto;
  width: 500px;
  padding: 30px;
  border: 1px solid #333;
  background-color: #fdfdfd;
}
<label>test
  <input autofocus />
</label>
<button>dummy button</button>
<hr/>
<button id="btn-show-modal">open modal</button>
<div class="modal">
  <div class="modal-content">
    <label>test
      <input autofocus />
    </label>
    <button id="btn-close-modal">close modal</button>
  </div>
</div>

Мы ищем tabIndex > -1 так что мы можем сосредоточиться на табулируемых элементах. Вы можете дополнительно ограничить этот фильтр игнорированием скрытых элементов, но я оставлю это вам. В любом случае, список не должен быть очень большим.
‡ В качестве альтернативы, как в демоверсии, вы можете заполнить массив рядом функций, единственной целью которых является сброс tabIndex , Вы также можете полностью отказаться от массива и просто добавить data-original-tab-index приписать затронутые элементы... используя document.querySelectorAll("[data-original-tab-index]") чтобы получить их по факту.

Вот демонстрация, которая использует атрибуты данных для хранения оригинала tabIndex так что вам не нужно поддерживать свой собственный массив:

function isDescendant(ancestor, descendant) {
  do {
    if (descendant === ancestor) return true;
  } while (descendant = descendant.parentNode);
  return false;
}

var lastFocused;

document.getElementById("btn-show-modal").addEventListener("click", function(e) {
  lastFocused = document.activeElement;
  var modal = document.querySelector(".modal");
  Array.prototype.forEach.call(document.all, o => {
    if (o.tabIndex > -1 && !isDescendant(modal, o)) {
      o.dataset.originalTabIndex = o.tabIndex;
      o.tabIndex = -1;
    }
  });
  // show modal
  modal.classList.add("shown");
  // focus modal autofocus
  modal.querySelector("[autofocus]").focus();
});

document.getElementById("btn-close-modal").addEventListener("click", function(e) {
  // restore tabs
  Array.prototype.forEach.call(document.querySelectorAll("[data-original-tab-index]"), o => {
    o.tabIndex = o.dataset.originalTabIndex;
    delete o.dataset.originalTabIndex;
  });
  // hide modal
  document.querySelector(".modal").classList.remove("shown");
  // restore focus
  lastFocused && lastFocused.focus();
});
.modal {
  display: none;
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(128, 128, 128, .75);
}
.modal.shown {
  display: flex;
}
.modal-content {
  margin: auto;
  width: 500px;
  padding: 30px;
  border: 1px solid #333;
  background-color: #fdfdfd;
}
<label>test
  <input autofocus />
</label>
<button>dummy button</button>
<hr/>
<button id="btn-show-modal">open modal</button>
<div class="modal">
  <div class="modal-content">
    <label>test
      <input autofocus />
    </label>
    <button id="btn-close-modal">close modal</button>
  </div>
</div>

Увидеть HTMLElement. dataset

Как насчет ловли tab-key? На последнем элементе, а затем сосредоточить внимание на первом и наоборот с shift-tab

Это я использую в среде multi-modless-diaolog, чтобы держать фокус в диалоге, переключаясь между диалогами с помощью мыши или другой клавиши

inputs=".editing, input, textarea, button, a, select"
no_tab="[type='hidden'], :disabled"

$focusable=dlg.$form.find(inputs).not(no_tab)


$fa_first=$focusable.first()
$fa_last=$focusable.last()

$fa_last.on("keydown", (evt) =>
    if evt.keyCode==9 && ! evt.shiftKey
        $fa_first.focus()
        evt.preventDefault()
        false
)
$fa_first.on("keydown", (evt) =>
    if evt.keyCode==9 && evt.shiftKey
        $fa_last.focus()
        evt.preventDefault()
        false
)

Небольшое редактирование: заменил мою функцию on на "unibind()" (=.off(x).on(x)) через jQuery "on()"

В случае, если вы хотите ограничить фокус внутри дом "родителя"

parent.addEventListener('focusout', function(event) {
    event.stopPropagation();

    if (node.contains(event.relatedTarget)) {  // if focus moved to another 
                                                              parent descend
        return;
    }

    parent.focus();  // otherwise focus on parent or change to another dom
})

поддерживается всеми современными браузерами

Взгляните на плагин jQuery BlockUI. У них есть пример использования модального поля с двумя кнопками, и оно также ограничивает вкладки.

Он может работать, а может и не работать "из коробки" с вашими модальными окнами, но это стоит посмотреть вместо того, чтобы реализовывать ваши собственные обходные пути.

Когда это делать в React . Я могу передать tabIndex, как вы сказали в вопросе. И мне также нужно попробовать pointerEvents none css для моего невидимого раздела в моем случае.

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

Используя JQuery, я отключил все поля ввода в разных формах и элементах div, как только открывается модальное окно (кроме полей в самой модальной форме).

$('#formId :input').prop('disabled',true);

Когда модальная форма закрыта, вы можете снова включить элементы ввода.

Отключенные поля не учитываются при "закладке" вокруг вашей страницы.

Другие вопросы по тегам