1

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

 
})();
Alexander
  • 123
  • 1
  • 1
  • 9

1 Answers1

3

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>
GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • 2
    Thanks a lot for your answer. I should've considered that while we're still in the loop it simply overrides firstEl and lastEl variables, I just overlooked it. Been debugging it for the last couple of days. I'm still learning about accessibility, so your advice about aria-modal is definitely helpful. – Alexander Oct 22 '20 at 08:49
  • glad it helped, yeah sometimes just having someone else look at your code can save you days of headaches. – GrahamTheDev Oct 22 '20 at 08:53