12

Let's say I have a document full of focusable elements, either because they are innately focusable (like <input type="text">) or because they have tabindex="0" or the like.

Now let's say there's a section of my document that I want to display as a modal dialog box, and I don't want the user to be distracted by anything outside the dialog box. I would like for the tab key to cycle only through focusable elements inside the container element for the dialog box. What is the simplest way to do this?

If possible, I am looking for a solution that doesn't care what the contents of the dialog or the rest of the page are and doesn't try to modify them. That is, I don't want to make the elements outside of the dialog not focusable, for example. First, this requires making a reversible change and keeping track of state. Second, this requires knowing all the possible ways an element could be made focusable. This feels messy, fragile, and unscalable to me.

My first attempt looks like this, but works only in the forward direction (pressing Tab). It doesn't work in the reverse direction (pressing Shift+Tab).

<div>Focusable stuff outside the dialog.</div>
<div class="dialog" tabindex="0">
  <!-- Focus should be trapped inside this dialog while it's open -->
  <div class="content">
    Form contents and focusable stuff here.
  </div>
  <div class="last-focus" tabindex="0" onfocus="this.parentNode.focus()"></div>
</div>
<div>More focusable stuff outside the dialog.</div>

I'd rather see pure JavaScript solutions. If there is a means of doing this with a library such as jQuery, I would prefer a link to the library code that does this.

Chris Calo
  • 7,518
  • 7
  • 48
  • 64
  • The easiest way is to bind an 'focusin' event to the document. In the event handler, you check whether the current focus is within the dialog. If not, then reapply focus to some element within the dialog. Don't forget to remove the event binding on the document once the dialog closes. – Bjorn Aug 14 '15 at 08:28
  • @Bjorn, it sounds like you need to use a timer to focus a different element than the target element of a `focusin` event. While it's less code to do so, it sounds like it might be buggy if you don't get the timing right. You should post a separate answer if you get it working. – Chris Calo Aug 16 '15 at 15:42
  • You don't need a timer for that, you just respond to the focusin event. I got it working, but my code is depending on jQuery/Backbone/Marionette, so not a clear answer if you work with a different framework. – Bjorn Aug 17 '15 at 14:44

3 Answers3

13

In the interest of completeness, I'm taking the link to jQuery UI dialog that @Domenic provided and filling in the details.

To implement this in the jQuery fashion requires two things:

  1. Listening for Tab or Shift+Tab (on keydown) for the modal element that should trap focus. This is the only means of moving focus via the keyboard. (If you want to prevent mouse interaction with the rest of the document, that is a separate problem solved by covering it with an element to prevent any mouse events from getting through.)

  2. Finding all tabbable elements inside the modal element. These are a subset of all focusable elements, excluding those that have tabindex="-1".

Tab goes forward. Shift+Tab goes backwards. Any time Tab is pressed while the last tabbable element in the modal element is focused, the first should receive focus. Similarly, any time Shift+Tab is pressed while the first tabbable element is focused, the last should receive focus. This will keep focus inside the modal element.

The hard part is knowing which elements are tabbable. Since tabbable elements are all focusable elements that don't have tabindex="-1", then we need to know know which elements are focusable. Since there's no property to determine if an element is focusable, jQuery does it by hard-coding the following cases:

  • input, select, textarea, button, and object elements that aren't disabled.
  • a and area elements that have an href or have a numerical value for tabindex set.
  • any element that has a numerical value for tabindex set.

It's not enough to check for these three cases. jQuery goes on to ensure that the element is visible. This means both of the following must be true:

  • None of its ancestors are display: none.
  • The computed value of visibility is visible. This means that the nearest ancestor to have visibility set must have a value of visible. If no ancestor has visibility set, then the computed value is visible.

It should be noted that jQuery's :visible selector does not look correct for this implementation because it says "elements with visibility: hidden…are considered to be visible," but they are not focusable.

Chris Calo
  • 7,518
  • 7
  • 48
  • 64
1

The jQuery UI dialog does this by capturing keydown events, checking if they are for TAB or not, then manually focusing the correct element.

Domenic
  • 110,262
  • 41
  • 219
  • 271
0

The jqModal jQuery plugin does this out of the box by setting the modal option to true. The examples on this page with forms should show it. I remember going through the code to see what was happening and you could do it quite easily with plain JS.

Moin Zaman
  • 25,281
  • 6
  • 70
  • 74
  • Thanks, Moin. It looks like example 4 at http://dev.iceburg.net/jquery/jqModal/ attempts to trap focus inside the dialog box, but it doesn't quite work (there is a "JavaScript" link that gets focus). Nevertheless, it looks promising, but I'm not sure how it's achieving this. Can you point me to the source that handles this part? – Chris Calo Oct 24 '11 at 03:17
  • Do a search for uppercase 'L' in the plugin. It seems they are `keypress`, `keydown` and `mousedown` events. – Moin Zaman Oct 24 '11 at 05:04