多模态的焦点陷阱

Focus trap for multiple modals

我正在研究陷阱焦点模式功能,它适用于单个元素,但我无法让它适用于多个元素。它只关注最后一个模态。我知道我的循环有问题,我尝试捕获 activeElement 并添加条件(如果它等于聚焦元素)但没有结果。

这是CodePen example

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>