Focus trap for multiple modals

Tags: , , ,



I’m working on a trap focus modal functionality and it works well with a single element, but I can’t get it working with multiple elements. It traps focus only on last modal. I know there is something wrong with my loop, I tried to catch activeElement and add a condition if it’s equal to focused element but with no result.

Here is the 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');
        }       
            
  } 

 
})();

Answer

You made a minor mistake with your loop in mobileAccessibility function.

When you loop through the modals in the section if ( body.classList.contains( 'showing-search-modal' ) ) { you close the loop too early.

This means you are setting firstEl = input; as the last modal no matter what (as you are overriding it) and same for lastEl = close;

By simply moving the loop to include the tabkey checks it works as expected.

A few other considerations

Now there are loads of things that you still need to consider from an accessibility perspective.

Things like the fact that screen reader users navigate by headings, sections, links etc. so just capturing Tab is not sufficient.

For example: you need to sit the modals outside of the <main> and then use aria-hidden="true" on the <main> element when the modals are open to hide everything else from screen readers.

Oh and add aria-modal to your modal, see this answer I gave to understand why.

Fixed code

(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>


Source: stackoverflow