Ограничить фокусировку tabindex частью страницы
Ситуация:
У меня есть веб-страница, которая открывает модальные окна (лайтбоксы), которые содержат формы, в которые пользователь может вводить данные. Пользователи обычно перемещаются с помощью клавиатуры, переходя от одного поля к другому.
Проблема:
Когда модальное окно открывается, только окно активно, остальная часть страницы не доступна с помощью мыши, но элементы могут быть достигнуты с помощью вкладок из модального окна.
Вопрос:
Как я могу ограничить движение, используя кнопку табуляции только для элементов в окне формы?
Единственное, о чем я могу думать, это использовать Javascript для установки tabindex=-1
на всех элементах формы (и других фокусируемых элементах), когда модальное окно открыто, а затем установите tabindex
Значения возвращаются к своим предыдущим значениям, когда модальное окно закрыто. Есть ли более простой / лучший способ?
6 ответов
Нет, это единственный способ.
- Найти все элементы, которые имеют
tabIndex
лучше чем-1
† и не принадлежат вашему модалу. - Создать массив ‡ и заполнить его ссылками на каждый элемент вместе с его оригиналом
tabIndex
, - Установить каждый элемент
tabIndex
в-1
поэтому он больше не может получать фокус с клавиатуры. - Когда модальное диалоговое окно закрыто, выполните итерацию по массиву и восстановите исходный
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);
Когда модальная форма закрыта, вы можете снова включить элементы ввода.
Отключенные поля не учитываются при "закладке" вокруг вашей страницы.