Update plugin to account for hidden focus items

I use this plugin

export function trapFocus(element, cb) {
  var focusableEls = element.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),
      firstFocusableEl = focusableEls[0];  
      lastFocusableEl = focusableEls[focusableEls.length - 1],
      KEYCODE_TAB = 9,
      ESCAPE_TAB = 27;
  element.addEventListener('keydown', function(e) {
    var isTabPressed = (e.key === 'Tab' || e.keyCode === KEYCODE_TAB);
    var isEscPressed = (e.key === 'Escape' || e.keyCode === ESCAPE_TAB);
    if(!isTabPressed && !isEscPressed) { 
      return; 
    }
    if(isEscPressed) {
      this.callback = cb;
      this.callback();
    }
    if(e.shiftKey) /* shift + tab */ {
      if(document.activeElement === firstFocusableEl) {
        lastFocusableEl.focus();
        e.preventDefault();
      }
    } else /* tab */ {
      if(document.activeElement === lastFocusableEl) {
        firstFocusableEl.focus();
        e.preventDefault();
      }
    }
  });
};

This basically is used as following

  • Click a button and we manually shift focus to the first focusable element in a “container”.
  • We trap the focus in this container, ultimately you loop in the container until you click a “close button”, and it takes you back to the open button. This is part of our accessibility.

The issue arises if the last "focusable element is actually hidden.

E.g. the + sign on the last link (which is a button) indicates that there’s a hidden link. That being hidden breaks the plugin. I’m not sure how to even approach this in Javascript. It’s a dynamic situation and sometimes the last focusable element actually is visible :slight_smile: . There might be multiple links in that last dropdown.

Can you not test the element to see if it is hidden? element.style.visibility === 'hidden' is what is coming to mind. If it is not hidden, set focus. Otherwise move to the last element that is visible and set focus there.

Maybe I am not understanding. I am assuming here that in your example since the last element is hidden, it is not setting the “Info For…” element as the focusable element. If this is the scenario, again check if the last item is hidden, since it is, you would go to the previous element, check if it is hidden and since it is not, set that to be the item focused. If the last item is visible, then you would set the focus on that.

Either way, I think you just need to check if the element is hidden or not and know whether or not to focus it or some element before it. :slight_smile:

That’s a good idea, but how can I do that in vanilla JS?

So I’m thinking that if I can check for visibility, and move that variable line inside of the keydown so it theoretically updates each tab?

Is there a way to update my querySelectorAll to include visibility? Or filter the variable?

I seemingly have it.

export function trapFocus(element, cb) {
  const KEYCODE_TAB = 9,
      ESCAPE_TAB = 27;
  element.addEventListener('keydown', function(e) {
    var focusableEls = Array.from(element.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])')).filter(s =>
      !!( s.offsetWidth || s.offsetHeight || s.getClientRects().length )
    );
    var firstFocusableEl = focusableEls[0];  
        lastFocusableEl = focusableEls[focusableEls.length - 1]
    var isTabPressed = (e.key === 'Tab' || e.keyCode === KEYCODE_TAB);
    var isEscPressed = (e.key === 'Escape' || e.keyCode === ESCAPE_TAB);
    if(!isTabPressed && !isEscPressed) { 
      return; 
    }
    if(isEscPressed) {
      this.callback = cb;
      this.callback();
    }
    if(e.shiftKey) /* shift + tab */ {
      if(document.activeElement === firstFocusableEl) {
        lastFocusableEl.focus();
        e.preventDefault();
      }
    } else /* tab */ {
      if(document.activeElement === lastFocusableEl) {
        firstFocusableEl.focus();
        e.preventDefault();
      }
    }
  });
};

What I expressed is vanilla. The style property of an element is straight out vanilla JS.

Ideally yes, in the keydown you can check the visibility of the element an do what you like. You can also just put the visibility check in your filter callback function. If it is not visible, then just don’t include it in focusableEls.

No need to alter the selector (it is already horrendously long). Just put the check in your filter callback. s.style.visibility === 'visible'. If it doesn’t meet the check, then it it is filtered out.

Maybe off by a country mile, but if the style isn’t set inline e.g. <h1 style='visibility: hidden'> then won’t you want getComputedStyle

e.g.

css

.heading {
  visibility: hidden
}

html

<h1 class='heading'>This is a test</h1>

javascript

const heading = document.querySelector('.heading')

console.log(heading.style.visibility) // empty string
console.log(window.getComputedStyle(heading).visibility) // 'hidden'

Just a thought.

1 Like