Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to keep focus within modal dialog?

I'm developing an app with Angular and Semantic-UI. The app should be accessible, this means it should be compliant with WCAG 2.0. To reach this purpose the modals should keep focus within the dialog and prevents users from going outside or move with "tabs" between elements of the page that lays under the modal.

I have found some working examples, like the following:

  • JQuery dialog: https://jqueryui.com/dialog/#modal-confirmation
  • dialog HTML 5.1 element: https://demo.agektmr.com/dialog
  • ARIA modal dialog example: http://w3c.github.io/aria-practices/examples/dialog-modal/dialog.html (that I have reproduced on Plunker)

Here is my try to create an accessible modal with Semantic-UI: https://plnkr.co/edit/HjhkZg

As you can see I used the following attributes:

role="dialog"

aria-labelledby="modal-title"

aria-modal="true"

But they don't solve my issue. Do you know any way to make my modal keeping focus and lose it only when user click on cancel/confirm buttons?

like image 891
smartmouse Avatar asked Jun 09 '17 07:06

smartmouse


People also ask

What is Tabindex in modal?

The tabindex attribute specifies the tab order of an element (when the "tab" button is used for navigating).


4 Answers

There is currently no easy way to achieve this. The inert attribute was proposed to try to solve this problem by making any element with the attribute and all of it's children inaccessible. However, adoption has been slow and only recently did it land in Chrome Canary behind a flag.

Another proposed solution is making a native API that would keep track of the modal stack, essentially making everything not currently the top of the stack inert. I'm not sure the status of the proposal, but it doesn't look like it will be implemented any time soon.

So where does that leave us?

Unfortunately without a good solution. One solution that is popular is to create a query selector of all known focusable elements and then trap focus to the modal by adding a keydown event to the last and first elements in the modal. However, with the rise of web components and shadow DOM, this solution can no longer find all focusable elements.

If you always control all the elements within the dialog (and you're not creating a generic dialog library), then probably the easiest way to go is to add an event listener for keydown on the first and last focusable elements, check if tab or shift tab was used, and then focus the first or last element to trap focus.

If you're creating a generic dialog library, the only thing I have found that works reasonably well is to either use the inert polyfill or make everything outside of the modal have a tabindex=-1.

var nonModalNodes;

function openDialog() {    
  var modalNodes = Array.from( document.querySelectorAll('dialog *') );

  // by only finding elements that do not have tabindex="-1" we ensure we don't
  // corrupt the previous state of the element if a modal was already open
  nonModalNodes = document.querySelectorAll('body *:not(dialog):not([tabindex="-1"])');

  for (var i = 0; i < nonModalNodes.length; i++) {
    var node = nonModalNodes[i];

    if (!modalNodes.includes(node)) {

      // save the previous tabindex state so we can restore it on close
      node._prevTabindex = node.getAttribute('tabindex');
      node.setAttribute('tabindex', -1);

      // tabindex=-1 does not prevent the mouse from focusing the node (which
      // would show a focus outline around the element). prevent this by disabling
      // outline styles while the modal is open
      // @see https://www.sitepoint.com/when-do-elements-take-the-focus/
      node.style.outline = 'none';
    }
  }
}

function closeDialog() {

  // close the modal and restore tabindex
  if (this.type === 'modal') {
    document.body.style.overflow = null;

    // restore or remove tabindex from nodes
    for (var i = 0; i < nonModalNodes.length; i++) {
      var node = nonModalNodes[i];
      if (node._prevTabindex) {
        node.setAttribute('tabindex', node._prevTabindex);
        node._prevTabindex = null;
      }
      else {
        node.removeAttribute('tabindex');
      }
      node.style.outline = null;
    }
  }
}
like image 106
Steven Lambert Avatar answered Oct 16 '22 10:10

Steven Lambert


The different "working examples" do not work as expected with a screenreader.

They do not trap the screenreader visual focus inside the modal.

For this to work, you have to :

  1. Set the aria-hidden attribute on any other nodes
  2. disable keyboard focusable elements inside those trees (links using tabindex=-1, controls using disabled, ...)

    • The jQuery :focusable pseudo selector can be useful to find focusable elements.
  3. add a transparent layer over the page to disable mouse selection.

    • or you can use the css pointer-events: none property when the browser handles it with non SVG elements, not in IE
like image 44
Adam Avatar answered Oct 16 '22 09:10

Adam


This focus-trap plugin is excellent at making sure that focus stays trapped inside of dialogue elements.

like image 3
Daniel Tonon Avatar answered Oct 16 '22 09:10

Daniel Tonon


It sounds like your problem can be broken down into 2 categories:

  1. focus on dialog box

Add a tabindex of -1 to the main container which is the DOM element that has role="dialog". Set the focus to the container.

  1. wrapping the tab key

I found no other way of doing this except by getting the tabbable elements within the dialog box and listening it on keydown. When I know the element in focus (document.activeElement) is the last one on the list, I make it wrap

like image 2
Marwan Avatar answered Oct 16 '22 08:10

Marwan