多模态的焦点陷阱
Focus trap for multiple modals
我正在研究陷阱焦点模式功能,它适用于单个元素,但我无法让它适用于多个元素。它只关注最后一个模态。我知道我的循环有问题,我尝试捕获 activeElement 并添加条件(如果它等于聚焦元素)但没有结果。
HTML
<div class="container">
<div class="nav__mobile">
<div class="nav__right-item">
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Mobile Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search">
<span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search"><span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
JavaScript
(function(){
var html = document.querySelector('html'),
body = document.body;
mobileAccessibility();
menuSearch();
function mobileAccessibility() {
document.addEventListener('keydown', function(e) {
var tabKey, shiftKey, selectors, activeEl, lastEl, firstEl;
if ( body.classList.contains('showing-modal') ) {
selectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
activeEl = document.activeElement;
// Search
if ( body.classList.contains( 'showing-search-modal' ) ) {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
for ( var i = 0; i < search.length; i++ ) {
var input = search[i].querySelector('.search-input');
var close = search[i].querySelector('.everse-menu-search-modal__close');
firstEl = input;
lastEl = close;
}
}
tabKey = e.key === 'Tab' || e.keyCode === 9;
shiftKey = e.shiftKey
if ( ! shiftKey && tabKey && lastEl === activeEl ) {
e.preventDefault();
firstEl.focus();
}
if ( shiftKey && tabKey && firstEl === activeEl ) {
e.preventDefault();
lastEl.focus();
}
}
});
}
function menuSearch() {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
if ( ! search.length > 0 ) {
return;
}
for ( var i = 0; i < search.length; i++ ) {
let trigger = search[i].querySelector('.everse-menu-search__trigger'),
modal = search[i].querySelector('.everse-menu-search-modal'),
inner = search[i].querySelector('.everse-menu-search-modal__inner'),
input = search[i].querySelector('.search-input'),
close = search[i].querySelector('.everse-menu-search-modal__close');
trigger.addEventListener('click', function(e) {
e.preventDefault();
body.classList.toggle('showing-modal');
body.classList.toggle('showing-search-modal');
modal.classList.add('everse-menu-search-modal--is-open');
setTimeout(() => {
input.focus();
}, 200);
});
inner.addEventListener('click', function(e) {
e.stopPropagation();
});
modal.addEventListener('click', function(e) {
closeModal(this);
});
close.addEventListener('click', function(e) {
closeModal(modal);
});
/*
* Close on click or on esc.
*/
document.addEventListener('keyup', function(e) {
if ( 27 === e.keyCode ) {
closeModal(modal);
}
});
}
function closeModal(modal) {
body.classList.remove('showing-modal');
body.classList.remove('showing-search-modal');
modal.classList.remove('everse-menu-search-modal--is-open');
}
}
})();
你在 mobileAccessibility
函数的循环中犯了一个小错误。
当您循环遍历 if ( body.classList.contains( 'showing-search-modal' ) ) {
部分中的模态框时,您过早地关闭了循环。
这意味着您无论如何都将 firstEl = input;
设置为最后一个模态(因为您正在覆盖它)并且 lastEl = close;
也是如此
通过简单地移动循环以包含 tabkey
检查它是否按预期工作。
其他一些注意事项
从可访问性的角度来看,您仍然需要考虑很多事情。
屏幕 reader 用户通过标题、部分、链接等进行导航,因此仅捕获 Tab 是不够的。
例如:您需要将模态框放在 <main>
之外,然后在模态框打开时在 <main>
元素上使用 aria-hidden="true"
以隐藏屏幕上的所有其他内容 readers.
哦还有 add aria-modal
to your modal, see this answer I gave to understand why.
固定代码
(function(){
var html = document.querySelector('html'),
body = document.body;
mobileAccessibility();
menuSearch();
function mobileAccessibility() {
document.addEventListener('keydown', function(e) {
var tabKey, shiftKey, selectors, activeEl, lastEl, firstEl;
if ( body.classList.contains('showing-modal') ) {
selectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
activeEl = document.activeElement;
// Search
if ( body.classList.contains( 'showing-search-modal' ) ) {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
for ( var i = 0; i < search.length; i++ ) {
var input = search[i].querySelector('.search-input');
var close = search[i].querySelector('.everse-menu-search-modal__close');
firstEl = input;
lastEl = close;
//moved the loop ending from here
tabKey = e.key === 'Tab' || e.keyCode === 9;
shiftKey = e.shiftKey
if ( ! shiftKey && tabKey && lastEl === activeEl ) {
e.preventDefault();
firstEl.focus();
}
if ( shiftKey && tabKey && firstEl === activeEl ) {
e.preventDefault();
lastEl.focus();
}
//placed the loop ending here so `firstEl` and `lastEl` now correspond to `search[i]` rather than the last item in `search`
}
}
}
});
}
function menuSearch() {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
if ( ! search.length > 0 ) {
return;
}
for ( var i = 0; i < search.length; i++ ) {
let trigger = search[i].querySelector('.everse-menu-search__trigger'),
modal = search[i].querySelector('.everse-menu-search-modal'),
inner = search[i].querySelector('.everse-menu-search-modal__inner'),
input = search[i].querySelector('.search-input'),
close = search[i].querySelector('.everse-menu-search-modal__close');
trigger.addEventListener('click', function(e) {
e.preventDefault();
body.classList.toggle('showing-modal');
body.classList.toggle('showing-search-modal');
modal.classList.add('everse-menu-search-modal--is-open');
setTimeout(() => {
input.focus();
}, 200);
});
inner.addEventListener('click', function(e) {
e.stopPropagation();
});
modal.addEventListener('click', function(e) {
closeModal(this);
});
close.addEventListener('click', function(e) {
closeModal(modal);
});
/*
* Close on click or on esc.
*/
document.addEventListener('keyup', function(e) {
if ( 27 === e.keyCode ) {
closeModal(modal);
}
});
}
function closeModal(modal) {
body.classList.remove('showing-modal');
body.classList.remove('showing-search-modal');
modal.classList.remove('everse-menu-search-modal--is-open');
}
}
})();
/*-------------------------------------------------------*/
/* Search
/*-------------------------------------------------------*/
.search-form {
position: relative;
}
.search-form label {
display: flex;
margin-bottom: 0;
font-family: inherit;
}
.everse-menu-search {
margin-top: 40px;
}
.everse-menu-search__trigger {
color: #666666;
}
.everse-menu-search__icon {
display: block;
}
.everse-menu-search-modal {
background-color: transparent;
position: fixed;
overflow: hidden;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 999;
visibility: hidden;
opacity: 0;
transition: all;
}
.everse-menu-search-modal__inner {
background-color: #fff;
transition: all;
transform: scale(1, 0);
transform-origin: 100% 0;
padding: 40px 0;
}
.everse-menu-search-modal__inner .search-input {
margin-bottom: 0;
border: 0;
outline: 0;
border-bottom: 1px solid #bebebe;
}
.everse-menu-search-modal__close {
position: absolute;
top: 0;
right: 0;
width: 56px;
height: 56px;
padding: 0;
border: 0;
text-align: center;
background-color: transparent;
color: #666666;
}
.everse-menu-search-modal__close:focus {
background-color: transparent;
color: initial;
}
.everse-menu-search-modal--is-open {
background-color: rgba(0, 0, 0, 0.5);
opacity: 1;
visibility: visible;
}
.everse-menu-search-modal--is-open .everse-menu-search-modal__inner {
transform: scale(1, 1);
}
<div class="container">
<div class="nav__mobile">
<div class="nav__right-item">
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Mobile Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search">
<span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search"><span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
我正在研究陷阱焦点模式功能,它适用于单个元素,但我无法让它适用于多个元素。它只关注最后一个模态。我知道我的循环有问题,我尝试捕获 activeElement 并添加条件(如果它等于聚焦元素)但没有结果。
HTML
<div class="container">
<div class="nav__mobile">
<div class="nav__right-item">
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Mobile Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search">
<span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search"><span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
JavaScript
(function(){
var html = document.querySelector('html'),
body = document.body;
mobileAccessibility();
menuSearch();
function mobileAccessibility() {
document.addEventListener('keydown', function(e) {
var tabKey, shiftKey, selectors, activeEl, lastEl, firstEl;
if ( body.classList.contains('showing-modal') ) {
selectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
activeEl = document.activeElement;
// Search
if ( body.classList.contains( 'showing-search-modal' ) ) {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
for ( var i = 0; i < search.length; i++ ) {
var input = search[i].querySelector('.search-input');
var close = search[i].querySelector('.everse-menu-search-modal__close');
firstEl = input;
lastEl = close;
}
}
tabKey = e.key === 'Tab' || e.keyCode === 9;
shiftKey = e.shiftKey
if ( ! shiftKey && tabKey && lastEl === activeEl ) {
e.preventDefault();
firstEl.focus();
}
if ( shiftKey && tabKey && firstEl === activeEl ) {
e.preventDefault();
lastEl.focus();
}
}
});
}
function menuSearch() {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
if ( ! search.length > 0 ) {
return;
}
for ( var i = 0; i < search.length; i++ ) {
let trigger = search[i].querySelector('.everse-menu-search__trigger'),
modal = search[i].querySelector('.everse-menu-search-modal'),
inner = search[i].querySelector('.everse-menu-search-modal__inner'),
input = search[i].querySelector('.search-input'),
close = search[i].querySelector('.everse-menu-search-modal__close');
trigger.addEventListener('click', function(e) {
e.preventDefault();
body.classList.toggle('showing-modal');
body.classList.toggle('showing-search-modal');
modal.classList.add('everse-menu-search-modal--is-open');
setTimeout(() => {
input.focus();
}, 200);
});
inner.addEventListener('click', function(e) {
e.stopPropagation();
});
modal.addEventListener('click', function(e) {
closeModal(this);
});
close.addEventListener('click', function(e) {
closeModal(modal);
});
/*
* Close on click or on esc.
*/
document.addEventListener('keyup', function(e) {
if ( 27 === e.keyCode ) {
closeModal(modal);
}
});
}
function closeModal(modal) {
body.classList.remove('showing-modal');
body.classList.remove('showing-search-modal');
modal.classList.remove('everse-menu-search-modal--is-open');
}
}
})();
你在 mobileAccessibility
函数的循环中犯了一个小错误。
当您循环遍历 if ( body.classList.contains( 'showing-search-modal' ) ) {
部分中的模态框时,您过早地关闭了循环。
这意味着您无论如何都将 firstEl = input;
设置为最后一个模态(因为您正在覆盖它)并且 lastEl = close;
通过简单地移动循环以包含 tabkey
检查它是否按预期工作。
其他一些注意事项
从可访问性的角度来看,您仍然需要考虑很多事情。
屏幕 reader 用户通过标题、部分、链接等进行导航,因此仅捕获 Tab 是不够的。
例如:您需要将模态框放在 <main>
之外,然后在模态框打开时在 <main>
元素上使用 aria-hidden="true"
以隐藏屏幕上的所有其他内容 readers.
哦还有 add aria-modal
to your modal, see this answer I gave to understand why.
固定代码
(function(){
var html = document.querySelector('html'),
body = document.body;
mobileAccessibility();
menuSearch();
function mobileAccessibility() {
document.addEventListener('keydown', function(e) {
var tabKey, shiftKey, selectors, activeEl, lastEl, firstEl;
if ( body.classList.contains('showing-modal') ) {
selectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
activeEl = document.activeElement;
// Search
if ( body.classList.contains( 'showing-search-modal' ) ) {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
for ( var i = 0; i < search.length; i++ ) {
var input = search[i].querySelector('.search-input');
var close = search[i].querySelector('.everse-menu-search-modal__close');
firstEl = input;
lastEl = close;
//moved the loop ending from here
tabKey = e.key === 'Tab' || e.keyCode === 9;
shiftKey = e.shiftKey
if ( ! shiftKey && tabKey && lastEl === activeEl ) {
e.preventDefault();
firstEl.focus();
}
if ( shiftKey && tabKey && firstEl === activeEl ) {
e.preventDefault();
lastEl.focus();
}
//placed the loop ending here so `firstEl` and `lastEl` now correspond to `search[i]` rather than the last item in `search`
}
}
}
});
}
function menuSearch() {
let search = document.querySelectorAll('.everse-menu-search:not(.eversor-menu-search)');
if ( ! search.length > 0 ) {
return;
}
for ( var i = 0; i < search.length; i++ ) {
let trigger = search[i].querySelector('.everse-menu-search__trigger'),
modal = search[i].querySelector('.everse-menu-search-modal'),
inner = search[i].querySelector('.everse-menu-search-modal__inner'),
input = search[i].querySelector('.search-input'),
close = search[i].querySelector('.everse-menu-search-modal__close');
trigger.addEventListener('click', function(e) {
e.preventDefault();
body.classList.toggle('showing-modal');
body.classList.toggle('showing-search-modal');
modal.classList.add('everse-menu-search-modal--is-open');
setTimeout(() => {
input.focus();
}, 200);
});
inner.addEventListener('click', function(e) {
e.stopPropagation();
});
modal.addEventListener('click', function(e) {
closeModal(this);
});
close.addEventListener('click', function(e) {
closeModal(modal);
});
/*
* Close on click or on esc.
*/
document.addEventListener('keyup', function(e) {
if ( 27 === e.keyCode ) {
closeModal(modal);
}
});
}
function closeModal(modal) {
body.classList.remove('showing-modal');
body.classList.remove('showing-search-modal');
modal.classList.remove('everse-menu-search-modal--is-open');
}
}
})();
/*-------------------------------------------------------*/
/* Search
/*-------------------------------------------------------*/
.search-form {
position: relative;
}
.search-form label {
display: flex;
margin-bottom: 0;
font-family: inherit;
}
.everse-menu-search {
margin-top: 40px;
}
.everse-menu-search__trigger {
color: #666666;
}
.everse-menu-search__icon {
display: block;
}
.everse-menu-search-modal {
background-color: transparent;
position: fixed;
overflow: hidden;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
z-index: 999;
visibility: hidden;
opacity: 0;
transition: all;
}
.everse-menu-search-modal__inner {
background-color: #fff;
transition: all;
transform: scale(1, 0);
transform-origin: 100% 0;
padding: 40px 0;
}
.everse-menu-search-modal__inner .search-input {
margin-bottom: 0;
border: 0;
outline: 0;
border-bottom: 1px solid #bebebe;
}
.everse-menu-search-modal__close {
position: absolute;
top: 0;
right: 0;
width: 56px;
height: 56px;
padding: 0;
border: 0;
text-align: center;
background-color: transparent;
color: #666666;
}
.everse-menu-search-modal__close:focus {
background-color: transparent;
color: initial;
}
.everse-menu-search-modal--is-open {
background-color: rgba(0, 0, 0, 0.5);
opacity: 1;
visibility: visible;
}
.everse-menu-search-modal--is-open .everse-menu-search-modal__inner {
transform: scale(1, 1);
}
<div class="container">
<div class="nav__mobile">
<div class="nav__right-item">
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Mobile Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search">
<span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="everse-menu-search">
<a href="#" class="everse-menu-search__trigger" title="Search">Search</a>
<div class="everse-menu-search-modal">
<div class="everse-menu-search-modal__inner">
<div class="container">
<form role="search" method="get" class="search-form relative" action="//localhost:3000/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-input" placeholder="Search" value="" name="s">
</label>
<button type="button" class="everse-menu-search-modal__close" aria-label="Close Search"><span>Close</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>