2

I am trying to create an accessible navigation menu for a portfolio-style website. When the screen is less than a certain width (768px for this demo) the horizontal menu navigation disappears and is replaced with a 'burger'. The burger's parent div has a JavaScript onclick and onkeydown function so that when the burger is clicked or tapped, or a keyboard user focuses on it and uses presses spacebar or enter, a div 'sidenav' opens up from the side containing the vertical navigation menu.

However, if a keyboard user continues tabbing, the focus carries on down the page behind the navigation as if 'sidenav' was not there and when the tabbing eventually finds its way to the top of the sidenav, the first element it encounters is the close button and tabbing closes the menu, so there's no way of reaching the menu items.

So: Question 1: How can I trigger the focus to 'jump' to the newly opened navigation when the burger is keyed down?

Question 2: How can I make the 'close' button ignore tabbing and only work with the spacebar or enter key?

Here is a simplified version of my menu (you may need to go full screen to see the horizontal menu).

/* detect keyboard users */
function handleFirstTab(e) {
    if (e.keyCode === 9) { // the "I am a keyboard user" key
        document.body.classList.add('user-is-tabbing');
        window.removeEventListener('keydown', handleFirstTab);
    }
}
window.addEventListener('keydown', handleFirstTab);

/*  Open Sidenav 
-------------------*/
function openNav() {
    let element = document.querySelector('ul.menucontent');
    if (element.classList.contains('menucontent')) {
    element.classList.remove('menu-a');
    element.classList.add('menu-b');
    };
    let element3 = document.querySelector('div.sidenav');
    let element4 = document.querySelector('.closebtn');
    if (element3.classList.contains('sidenav')) {
    element3.style.width = "350px";
    element4.style.visibility = "visible";
    };
  document.getElementById('vmenu').focus();
}
function closeNav() {
    let element = document.querySelector('ul.menucontent');
    if (element.classList.contains('menucontent')) {
    element.classList.remove('menu-b');
    element.classList.add('menu-a');
    };
    let element3 = document.querySelector('div.sidenav');
    let element4 = document.querySelector('.closebtn');
    if (element3.classList.contains('sidenav')) {
    element3.style.width = "0";
    element4.style.visibility = "hidden";
    };
}
// Toggle content  
    for (const selector of [".toggle-btn",]) {
        const toggleButtons = [...document.querySelectorAll(selector)];
        for (const toggleButton of toggleButtons) {
            toggleButton.addEventListener('click', () => {
                toggleButtons.filter(b => b !== toggleButton).forEach(b => {
                    b.nextElementSibling.classList.remove('reveal-content');
                });
                toggleButton.nextElementSibling.classList.toggle('reveal-content');
            });
        };
    }
    for (const selectorTwo of [".close-btn",]) {
        const closeButtons = [...document.querySelectorAll(selectorTwo)];
        for (const closeButton of closeButtons) {
            closeButton.addEventListener('click', () => {
                closeButton.parentElement.classList.toggle('reveal-content');
            });
        };
    }
body.user-is-tabbing button > a:focus {
  border: none;
}
body:not(.user-is-tabbing) a:focus,
body:not(.user-is-tabbing) button:focus,
body:not(.user-is-tabbing) input:focus,
body:not(.user-is-tabbing) select:focus,
body:not(.user-is-tabbing) textarea:focus {
  outline: none;
}
.container-fluid, 
.container {
  margin-right: auto;
  margin-left: auto;
  width: 100%;
}
.d-block
.d-none {
  display: none;
}
.sidenav {
  height: 100%;
  width: 0;
  position: fixed;
  z-index: 996;
  top: 0;
  left: 0;
  background-color: #fff;
  overflow-x: hidden;
  transition: 0.5s;
  padding: 1rem 0 0;
  box-shadow: 0 2px 5px #acaaaa;
}
.trigram {
  position: relative;
  top: 0;
  left: 0;
  margin-bottom: 1rem;
  padding: 0;
  background: transparent;
  z-index: 995;
  width: 2rem;
}
.burger {
  position: relative;
  border-top: 0.15rem solid green;
  border-bottom: 0.15rem solid green;
  background: transparent;
  height: 1.5rem;
  width: 2rem;
}
.burger::after {
  position: absolute;
  content: "";
  border-top: 0.15rem solid green;
  top: 40%;
  left: 0;
  width: 2rem;
}
.sidemenu {
  position: relative;
  top: 5rem;
}
.mm ul {
  list-style: none;
}
.mm li {
  margin: 1rem 0;
  padding: 0 0 0 1rem;
}
.mm .menucontent.menu-a {
  display: none;
}
.mm .menucontent.menu-b {
  display: flex;
  display: -webkit-flex;
  flex-direction: column;
  justify-content: normal;
  margin: 0 0 1rem;
  padding: 0;
  position: relative;
  top: 0;
  z-index: 997;
  overflow-y: auto;
  }
.closebtn {
  border-bottom: none;
  font-size: 2.25rem;
  margin: 0;
  position: absolute;
  top: 2rem;
  right: 2rem;
  z-index: 998;
}
@media only screen and (min-width: 768px){
  .d-none {
    display: none;
  }
  .d-md-block {
    display: block;
  }
  .trigram {
    display: none;
  }
  .mm .menucontent.menu-a {
    position: relative;
    padding: 0;
    margin: 0 auto;
    white-space: nowrap;
    display: flex;
  }
  .mm .menucontent.menu-a,
  .mm .menucontent.menu-b {
    flex-direction: row;
    justify-content: center;
  }
  .mm li {
    padding: 0 0.5rem;
  }
   .main-menu-container {
    border-top: 2px solid green;
    border-bottom: 2px solid green;
    padding: 0.5rem 0;
    margin: 1rem 0;
  }
}
<div id="sidenav" class="sidenav">
    <div id="closebtn" class="closebtn">
        <a href="javascript:void(0)" onclick="closeNav()" onkeydown="closeNav()" role="button" tabindex="0" aria-label="close navigation">&times;</a>
    </div>
    <div id="vmenu" class="sidemenu d-md-none mm">
        <nav aria-label="Main Navigation" class="menuouter ">
            <ul class="menucontent menu-a" role="menubar">
                <li class="item-101 default current active single top-level" role="none" tabindex="-1">
                    <a href="#" title="Side menu Home" class="icon-home">Side menu Home</a>
                </li>
                <li class="item-128 single" role="none" tabindex="-1">
                    <a href="#" title="Side menu page 2">Side menu page 2</a>
                </li>
            </ul>
        </nav>
    </div>
</div>
<div class="container-fluid menu-outer d-block d-md-none">
    <div id="trigram" class="trigram" role="button" tabindex="0" aria-label="open navigation" aria-controls="sidenav" aria-haspopup="true" onclick="openNav()" onkeydown="openNav()">
        <div class="burger" style="cursor:pointer" >&nbsp;</div>
    </div>
</div>
<div class="container-fluid d-none d-md-block">
    <div class="main-menu-container">
        <div id="hmenu" class="row d-none d-md-block main-menu mm">
            <nav aria-label="Main Navigation" class="menuouter ">
                <ul class="menucontent menu-a" role="menubar">
                    <li class="item-101 default current active single top-level" role="none" tabindex="-1">
                        <a href="#" title="Horizontal menu Home" class="icon-home">Horizontal menu Home</a>
                    </li>
                    <li class="item-128 single" role="none" tabindex="-1">
                        <a href="#" title="Horizontal menu page 2">Horizontal menu page 2</a>
                    </li>
                </ul>
            </nav>
        </div>
    </div>
</div>
<div>This is some text. It has a link in it: <a href="#">This is the first link</a></div>
<div>Here is some more text with more links. It has a link in it: <a href="#">This is the second link</a>. Integer mauris sem, convallis ut, consequat in, sollicitudin sed, leo.</div>
<div>Sed lacus velit, consequat in, ultricies sit amet, malesuada et, diam. Integer mauris sem, convallis ut, consequat in, sollicitudin sed, leo. <a href="#">This is the third link </a>Cras purus elit, hendrerit ut, egestas eget, sagittis at, nulla. Integer justo dui, faucibus dictum, convallis sodales, accumsan id, risus. Aenean risus. Vestibulum scelerisque placerat sem.</div>
Gillian
  • 287
  • 2
  • 11

3 Answers3

2

Good articles on 'focus trapping': https://medium.com/@im_rahul/focus-trapping-looping-b3ee658e5177
Here's a stackoverflow answer that might help: Vanilla javascript Trap Focus in modal (accessibility tabbing )

This is how I did it for a modal:

const btnOpenEmailSignup = document.getElementById('env'); //This is the button that opens the modal
const modalOverlay = document.getElementById('modalOpenEmailSignup'); //Modal specific--won't need!
const btnClose = document.getElementById('close'); //Modal specific--won't need!
let focusedElementBeforeModal;
const toggleModal = function modalToggel() {
  modalOverlay.classList.toggle('show-modal'); //Modal specific--won't need!

  // ***** Trap focus ***** //
  // Save current focus--*You might not need this*--
  focusedElementBeforeModal = document.activeElement;

  // Listen and trap the keyboard
  modal.addEventListener('keydown', trapTabKey);

  // Find all focusable children (not all of these will be needed, but keeping them in shouldn't hurt)
  const focusableElementsString = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
  let focusableElements = modal.querySelectorAll(focusableElementsString);

  // convert NodeList to Array
  focusableElements = Array.prototype.slice.call(focusableElements);
  const emailField = document.getElementById('email'); //This const is the one that will get focus on opening the modal.
  const firstTabStop = focusableElements[0];
  var lastTabStop = focusableElements[focusableElements.length - 1];

  // Focus to email field
  emailField.focus();

  function trapTabKey(e) {
    // Check for tab key press
    if (e.keyCode === 9) {
      // SHIFT + TAB
      if (e.keyShift) {
        if (document.activeElement === firstTabStop) {
          e.preventDefault();
          lastTabStop.focus();
        }
      // TAB
      } else {
        if (document.activeElement === lastTabStop) {
          e.preventDefault();
          firstTabStop.focus();
        }
      }
    }
  }
  // ***** Trap focus end ***** //

One note: This only seems to work in one direction ('down', with a tab press, as opposed to 'up' with shift+tab). Haven't been able to make it work the other way yet.

Dharman
  • 30,962
  • 25
  • 85
  • 135
AVD
  • 53
  • 1
  • 8
2

Question 2: How can I make the 'close' button ignore tabbing and only work with the spacebar or enter key?

Starting with this question as it is more complex, I assume it is meant to be the same as "how do I trap focus within my menu" given the rest of the question text.

You will often see examples of people trapping focus by intercepting the Tab key and Shift + Tab keys, but not doing anything else.

The problem is that this does not work for screen reader users as they use shortcuts to navigate via headings, links, sections etc.

So we have to hide all other content other than your menu from screen readers.

Most of the work can be done with aria-hidden="true" on the <main>, <aside> and <footer> elements etc. to hide all of the page content.

However we still have problem that any elements that can receive focus (buttons, hyperlinks etc.) are still focusable and so ignore aria-hidden.

For this we need to add tabindex="-1" to every focusable element on the page except for those within our menu.

The menu should also be closable via the Esc key.

Finally when the menu closes focus should be returned to the button that opened it, this is especially important if the menu is closed via the Esc key as the document focus will be lost.

The below example is a little messy but it should cover all of the main points. I have commented why each item exists and what it does so hopefully it makes sense.

When you open the menu inspect the other items on the page to see that they have all had tabindex="-1" added to them.

/////////////////////////////////////////HIDING ALL OTHER CONTENT FROM SCREEN READERS///////////////////////////////////
var content = document.getElementById('contentDiv');
var menuBtn = document.querySelector('.open-menu');
var closeMenuBtn = document.querySelector('.close-menu');
var menu = document.querySelector('.menu');
var focusableItems = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', '[tabindex]:not([disabled])', '[contenteditable=true]:not([disabled])'];

//the main function for setting the tabindex to -1 for all children of a parent with given ID (and reversing the process)
function hideOrShowAllInteractiveItems(parentDivID){  

  //build a query string that targets the parent div ID and all children elements that are in our focusable items list.
  var queryString = "";
  for (i = 0, leni = focusableItems.length; i < leni; i++) {
    queryString += "#" + parentDivID + " " + focusableItems[i] + ", ";
  }
  queryString = queryString.replace(/,\s*$/, "");
      
  var focusableElements = document.querySelectorAll(queryString);      
  for (j = 0, lenj = focusableElements.length; j < lenj; j++) {
            
    var el = focusableElements[j];
    if(!el.hasAttribute('data-modified')){ // we use the 'data-modified' attribute to track all items that we have applied a tabindex to (as we can't use tabindex itself).
            
      // we haven't modified this element so we grab the tabindex if it has one and store it for use later when we want to restore.
      if(el.hasAttribute('tabindex')){
        el.setAttribute('data-oldTabIndex', el.getAttribute('tabindex'));
      }
              
      el.setAttribute('data-modified', true);
      el.setAttribute('tabindex', '-1'); // add `tabindex="-1"` to all items to remove them from the focus order.
              
    }else{
      //we have modified this item so we want to revert it back to the original state it was in.
      el.removeAttribute('tabindex');
      if(el.hasAttribute('data-oldtabindex')){
        el.setAttribute('tabindex', el.getAttribute('data-oldtabindex'));
        el.removeAttribute('data-oldtabindex');
      }
      el.removeAttribute('data-modified');
    }
  }
}


var globalVars = {};


function openMenu(){
     menu.classList.add("open");
     menuBtn.setAttribute('aria-expanded', true);
     
     //get all the focusable items in our menu and keep track of the button that opened the menu for when we close it again.
     setFocus(menuBtn, 'menu');
     
     content.setAttribute("aria-hidden", true);
}

function closeMenu(){
    //close menu
    //unhide the main content
    content.setAttribute("aria-hidden", false);
    //hide the menu
     menu.classList.remove("open");
     // set `aria-expanded` - important for screen reader users.
     menuBtn.setAttribute('aria-expanded', false);
     //set focus back to the button that opened the menu if we can
     if (globalVars.beforeOpen) {
        globalVars.beforeOpen.focus();
     }
}




//toggle the menu
menuBtn.addEventListener('click', function(){
  //use our function to add the relevant `tabindex="-1"` to all interactive elements outside of the menu.
  hideOrShowAllInteractiveItems('contentDiv');
  //check if the menu is open, if it is close it and reverse everything.
  openMenu();
});

closeMenuBtn.addEventListener('click', function(){
  //use our function to add the relevant `tabindex="-1"` to all interactive elements outside of the menu.
  hideOrShowAllInteractiveItems('contentDiv');
  //check if the menu is open, if it is close it and reverse everything.
  closeMenu();
});

//////////////////////////////////TRAPPING FOCUS//////////////////////////////////



var setFocus = function (item, className) { //we pass in the button that activated the menu and the className of the menu list, your menu must have a unique className for this to work.

    className = className || "content"; //defaults to class 'content' in case of error ("content" being the class on the <main> element.)
    globalVars.beforeOpen = item; //we store the button that was pressed before the modal opened in a global variable so we can return focus to it on modal close.

    var findItems = [];
    for (i = 0, len = focusableItems.length; i < len; i++) {
        findItems.push('.' + className + " " + focusableItems[i]); //add every focusable item to an array.
    }
    // finally add the open button to our list of focusable items as it sits outside our menu list. 
    


    var findString = findItems.join(", ");
    globalVars.canFocus = Array.prototype.slice.call(document.querySelectorAll(findString)); 
    if (globalVars.canFocus.length > 0) {
        globalVars.canFocus[0].focus(); //***set the focus to the first focusable element within the modal
        globalVars.lastItem = globalVars.canFocus[globalVars.canFocus.length - 1]; //we also store the last focusable item within the modal so we can keep focus within the modal. 
    }
}

//listen for keypresses and intercept both the Esc key (to close the menu) and tab and shift tab while the menu is open so we can manage focus.
document.onkeydown = function (evt) {
    evt = evt || window.event;
    if (evt.keyCode == 27) {
        //unhide the main content - exactly the same as in the btn event listener.
     hideOrShowAllInteractiveItems('contentDiv');
     closeMenu();
    }
  if (menu.classList.contains('open') && evt.keyCode == 9) { //global variable to check any modal is open and key is the tab key
        if (evt.shiftKey) { //also pressing shift key
            if (document.activeElement == globalVars.canFocus[0]) { //the current element is the same as the first focusable element
                evt.preventDefault();
                globalVars.lastItem.focus(); //we focus the last focusable element as we are reverse tabbing through the items.
            }
        } else {
        console.log(document.activeElement, globalVars.lastItem);
            if (document.activeElement == globalVars.lastItem) { //when tabbing forward we look for the last tabbable element 
                evt.preventDefault();
                
                globalVars.canFocus[0].focus(); //move the focus to the first tabbable element.
            }
        }
    }
};
.menu{
display: none;
}

.menu.open{
   display: block;
}
<header>
    <button class="open-menu">Menu</button>
    <nav>
    <ul class="menu">
        <li><button class="close-menu">Close Menu</button></li>
        <li><a href="https://google.com">Google</a></li>
        <li><a href="https://google.com">Google again</a></li>
        <li><a href="https://google.com">Google once more</a></li>
    </ul>
    </nav>
</header>
<div id="contentDiv">
  



  <main>
  <p>Some information</p>
  <input />
  <button>a button</button>
  </main>
  <footer>
  <button tabindex="1">a button with a positive tabindex that needs restoring</button>
  </footer>
</div>

So: Question 1: How can I trigger the focus to 'jump' to the newly opened navigation when the burger is keyed down?

Don't do this, instead leave the focus where it is and make the button that opens the menu part of the focusable items.

Also make sure to toggle aria-expanded="true" to let screen readers know that clicking the button has expanded some additional information. I have also done this in the example.

GrahamTheDev
  • 22,724
  • 2
  • 32
  • 64
  • Thanks again, Graham. However, I have a couple of queries: Firstly, on running this snippet discovered that the two non-menu buttons are retaining their `data-modified="true` status if 'Esc' is used to close the menu. If the 'spacebar' is used to close the menu then `data-modified="true` is removed. Secondly, I am trying to avoid using IDs for elements where there is a possibility that they could appear twice on a page. This could be the case where the content from another page is displayed on the current page. – Gillian Feb 07 '21 at 15:20
  • Thanks, I missed adding the `hideOrShowAllInteractiveItems('contentDiv');` in the Escape key part (all the parts to close would normally sit in a function rather than being duplicated both on button press and on the Esc key press). You would need either a unique ID or a unique class on the menu container, otherwise there is no way to identify the container. As your menu should not be duplicated if you use content from another page this shouldn't be an issue. If you wanted it to be a class just change the selector for the content div (as that should wrap **all** content) to a class selector. – GrahamTheDev Feb 07 '21 at 15:51
  • Then change the line `queryString += "#" + parentDivID + " " + focusableItems[i] + ", ";` to `queryString += "." + parentDivID + " " + focusableItems[i] + ", ";` and that should be everything you need. – GrahamTheDev Feb 07 '21 at 15:52
  • Thanks. What is the parentDivID that is being referred to? Is is the parentDivID of the menu? If so I can't see one in your demo or is it the parentDivID of the rest of the page content - i.e. `id="contentDiv"` that is being temporarily hidden? – Gillian Feb 07 '21 at 16:45
  • It is just the local variable from the function `hideOrShowAllInteractiveItems(parentDivID)`. as it was designed to take an element ID. Just have a quick read of that function and you will see where I am on about changing things and then look where the function is called and I think you will get it. – GrahamTheDev Feb 07 '21 at 17:16
  • Still struggling, as I need the open and close buttons to be separate elements as per my original snippet. The open button (trigram) is part of the main content and the close button (X) is inside the sidenav and only appears when the sidenav containing the menu opens. I've tried splitting your code into two, but am having trouble finding the correct JavaScript code for `classList.doesnotcontain('open')`. Sorry - my JS is very elementary. – Gillian Feb 08 '21 at 16:39
  • Changed the example to account for two different buttons, I need to double check it but it seems to work. – GrahamTheDev Feb 08 '21 at 17:08
  • I added the 'user-is-tabbing' JS you gave me the other day. Whilst the two separate buttons work (thank you), it's not possible to tab to the 'close' button when the menu is open. – Gillian Feb 08 '21 at 17:30
  • I'm also getting messages in VSC that `keyCode` is deprecated. – Gillian Feb 08 '21 at 17:33
  • Have you tried the updated version (you have to update the HTML as well as the JS) above? It works fine for me, bear in mind in your example the "menu" button would be hidden behind your drawer and the "Close menu" button is your "X" . As for `keyCode` being deprecated my apologies we work in IE9 a lot and `e.key` is not well supported there. You can use `.key = "Escape"` and `.key = "Tab"` etc. – GrahamTheDev Feb 08 '21 at 18:34
0

After a lot of code fiddling to customise the two answers above to my specific requirements - with many thanks for the guidance from Graham Ritchie - and also from AVD who provided a similar answer but less code - I managed to get the code to work exactly as I want, including using only CSS classes and no IDs to avoid potential duplicate IDs.

// detect keyboard users
function handleFirstTab(e) {
    if (e.key === "Tab") { // the "I am a keyboard user" key
        document.body.classList.add('user-is-tabbing');
        window.removeEventListener('keydown', handleFirstTab);
    }
}
window.addEventListener('keydown', handleFirstTab);
/////////////////////HIDING ALL OTHER CONTENT FROM SCREEN READERS///////////////////////
/// `var` changed to `let`
let content = document.querySelector('.mainContent');
let menuBtn = document.querySelector('.open-menu');
let closeMenuBtn = document.querySelector('.close-menu');
let openSidenav = document.querySelector('.sidenav');
let menu = document.querySelector('.menu');
let focusableItems = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', '[tabindex]:not([disabled])', '[contenteditable=true]:not([disabled])'];

//the main function for setting the tabindex to -1 for all children of a parent with given ID (and reversing the process)
function hideOrShowAllInteractiveItems(mainContent){

//build a query string that targets the parent div ID and all children elements that are in our focusable items list.
let queryString = "";
for (i = 0, leni = focusableItems.length; i < leni; i++) {
    queryString += "." + mainContent + " " + focusableItems[i] + ", ";
}
queryString = queryString.replace(/,\s*$/, "");
    
    let focusableElements = document.querySelectorAll(queryString);
    for (j = 0, lenj = focusableElements.length; j < lenj; j++) {

        let el = focusableElements[j];
        if(!el.hasAttribute('data-modified')){ // we use the 'data-modified' attribute to track all items that we have applied a tabindex to (as we can't use tabindex itself).

        // we haven't modified this element so we grab the tabindex if it has one and store it for use later when we want to restore.
            if(el.hasAttribute('tabindex')){
                el.setAttribute('data-oldTabIndex', el.getAttribute('tabindex'));
            }

            el.setAttribute('data-modified', true);
            el.setAttribute('tabindex', '-1'); // add `tabindex="-1"` to all items to remove them from the focus order.

            } 
        else {
            //we have modified this item so we want to revert it back to the original state it was in.
            el.removeAttribute('tabindex');
            if(el.hasAttribute('data-oldtabindex')){
                el.setAttribute('tabindex', el.getAttribute('data-oldtabindex'));
                el.removeAttribute('data-oldtabindex');
            }
            el.removeAttribute('data-modified');
        }
    }
}

let globalLets = {};

function openMenu(){
    menu.classList.add("open");
    openSidenav.classList.add('open-sidenav');
    menuBtn.setAttribute('aria-expanded', true);
    
    //get all the focusable items in our menu and keep track of the button that opened the menu for when we close it again.
    setFocus(menuBtn, 'menu');
    
    content.setAttribute("aria-hidden", true);
}

function closeMenu(){
    //close menu
    //unhide the main content
    content.removeAttribute("aria-hidden");
    //hide the menu
    menu.classList.remove("open");
    openSidenav.classList.remove('open-sidenav');
    // set `aria-expanded` - important for screen reader users.
    menuBtn.setAttribute('aria-expanded', false);
    //set focus back to the button that opened the menu if we can
    if (globalLets.beforeOpen) {
        globalLets.beforeOpen.focus();
    }
}

//toggle the menu
menuBtn.addEventListener('click', function(){
//use our function to add the relevant `tabindex="-1"` to all interactive elements outside of the menu.
hideOrShowAllInteractiveItems('mainContent');
//check if the menu is open, if it is close it and reverse everything.
openMenu();
});

closeMenuBtn.addEventListener('click', function(){
//use our function to add the relevant `tabindex="-1"` to all interactive elements outside of the menu.
hideOrShowAllInteractiveItems('mainContent');
//check if the menu is open, if it is close it and reverse everything.
closeMenu();
});

////// Additional Javascript to close the pop-out menu and revert hidden elements if a user increases the screen width, e.g. by rotating a screen from portrait to landscape, without closing the pop-out menu first.//////

window.addEventListener('resize', wideScreen);
function wideScreen() {
    let ww = window.matchMedia("(min-width: 992px)");
    if (ww.matches) {
        let outer = document.querySelector('.mainContent');
        if (outer.hasAttribute("aria-hidden", true)) { 
        hideOrShowAllInteractiveItems('mainContent');
        closeMenu();
        }
    }
}
//////////////////////////////////TRAPPING FOCUS//////////////////////////////////

let setFocus = function (item, className) { //we pass in the button that activated the menu and the className of the menu list, your menu must have a unique className for this to work.

    className = "sidenav" || "content"; //defaults to class 'content' in case of error ("content" being the class on the <main> element.)
    globalLets.beforeOpen = item; //we store the button that was pressed before the modal opened in a global letiable so we can return focus to it on modal close.

    let findItems = [];
    for (i = 0, len = focusableItems.length; i < len; i++) {
        findItems.push('.' + className + " " + focusableItems[i]); //add every focusable item to an array.
    }
    // finally add the open button to our list of focusable items as it sits outside our menu list. 
    

    let findString = findItems.join(", ");
    globalLets.canFocus = Array.prototype.slice.call(document.querySelectorAll(findString)); 
    if (globalLets.canFocus.length > 0) {
        globalLets.canFocus[0].focus(); //***set the focus to the first focusable element within the modal
        globalLets.lastItem = globalLets.canFocus[globalLets.canFocus.length - 1]; //we also store the last focusable item within the modal so we can keep focus within the modal. 
    }
}

//listen for keypresses and intercept both the Esc key (to close the menu) and tab and shift tab while the menu is open so we can manage focus.
document.onkeydown = function (evt) {
    evt = evt || window.event;
    if (evt.key === "Escape") {
        //unhide the main content - exactly the same as in the btn event listener.
     hideOrShowAllInteractiveItems('mainContent');
     closeMenu();
    }
  if (menu.classList.contains('open') && evt.key == "Tab") { //global letiable to check any modal is open and key is the tab key
        if (evt.shiftKey) { //also pressing shift key
            if (document.activeElement == globalLets.canFocus[0]) { //the current element is the same as the first focusable element
                evt.preventDefault();
                globalLets.lastItem.focus(); //we focus the last focusable element as we are reverse tabbing through the items.
            }
        } else {
        console.log(document.activeElement, globalLets.lastItem);
            if (document.activeElement == globalLets.lastItem) { //when tabbing forward we look for the last tabbable element 
                evt.preventDefault();
                
                globalLets.canFocus[0].focus(); //move the focus to the first tabbable element.
            }
        }
    }
};
body.user-is-tabbing button > a:focus {
  border: none;
}
body:not(.user-is-tabbing) a:focus,
body:not(.user-is-tabbing) button:focus,
body:not(.user-is-tabbing) input:focus,
body:not(.user-is-tabbing) select:focus,
body:not(.user-is-tabbing) textarea:focus {
  outline: none;
}
.menu{
display: none;
}
.menu.open{
  display: block;
}
.sidenav {
  display: none;
  width: 0;
  background-color: #fff;
  box-shadow: 0 2px 5px #acaaaa;
}
.sidenav.open-sidenav {
  display: block;
  position: fixed;
  height: 100%;
  top: 0;
  left: 0;
  width: 150px;
}
<div class="sidenav"><!-- the sidenav is hidden and invisible until the 'open' button is clicked/pressed -->
    <button class="close-menu">Close Menu</button>
    <!-- the close button is inside the sidenav but separate from the menu as the menu is dynamically created -->
    <ul class="menu">
        <li><a href="https://google.com">Google</a></li>
        <li><a href="https://bbc.co.uk">BBC</a></li>
        <li><a href="https://msn.com">MSN</a></li>
    </ul>
</div>
<div class="mainContent">
    <main>
        <p>Some information</p>
        <button class="open-menu">Menu</button><!-- the open button is within the main content -->
        <p>Some more information</p>
        <input />
        <button>a button</button>
    </main>
    <footer>
        <button tabindex="0">a button with a positive tabindex that needs restoring</button>
    </footer>
</div>
Gillian
  • 287
  • 2
  • 11