6

I've a web application with dialogs. A dialog is a simple div-container appended to the body. There is also an overlay for the whole page to prevent clicks to other controls. But: Currently the user can focus controls that are under the overlay (for example an input). Is there any way to limit the tabbable controls to those which are in the dialog?

I am using jQuery (but not using jQueryUI). In jQueryUi dialogs it's working (but I don't want to use jQueryUI). I failed to figure out, how this is accomplished there.

Here is the jQueryUI example: http://jqueryui.com/resources/demos/dialog/modal-confirmation.html - The link on the webpage is not focusable. The focus is kept inside the dialog (the user cannot focus the urlbar of the browser using tab).

HTML:

<a href="#test" onclick="alert('Oh no!');">I should not receive any focus</a>
<input type="text" value="No focus please" />
<div class="overlay">
    <div class="dialog">
        Here is my dialog<br />
        TAB out with Shift+Tab after focusing "focus #1"<br />
        <input type="text" value="focus #1" /><br />
        <input type="text" value="focus #1" /><br />
    </div>
</div>    

CSS:

.overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.3);
    text-align: center;
}

.dialog {
    display: inline-block;
    margin-top: 30%;
    padding: 10px;
    outline: 1px solid black;
    background-color: #cccccc;
    text-align: left;
}

Here is my fiddle: http://jsfiddle.net/SuperNova3000/weY4L/

Does anybody have an idea? I repeat: I don't want to use jQueryUI for this. I'd like to understand the underlying technique.

Stephen R
  • 3,512
  • 1
  • 28
  • 45
SuperNova
  • 2,792
  • 2
  • 25
  • 34

2 Answers2

5

I've found an easy solution for this issue after hours of trying. I think the best way is adding 2 pseudo elements. One before and one after the dialog (inside the overlay). I'm using <a>-Tags which are 0x0 pixels. When reaching the first <a>, I'm focusing the last control in the dialog. When focusing the last <a>, I'm focusing the first control in the dialog.

I've adapted the answer of this post: Is there a jQuery selector to get all elements that can get focus? - to find the first and last focusable control.

HTML:

<div class="overlay">
    <a href="#" class="focusKeeper">
    <div class="dialog">
        Here is my dialog<br />
        TAB out with Shift+Tab after focusing "focus #1"<br />
        <input type="text" value="focus #1" /><br />
        <input type="text" value="focus #1" /><br />
    </div>
    <a href="#" class="focusKeeper">
</div>    

Extra CSS:

.focusKeeper {
    width: 0;
    height: 0;
    overflow: hidden;
}

My Javascript:

$.fn.getFocusableChilds = function() {
  return $(this)
    .find('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object:not([disabled]), embed, *[tabindex], *[contenteditable]')
    .filter(':visible');
};

[...]

$('.focusKeeper:first').on('focus', function(event) {
    event.preventDefault();
    $('.dialog').getFocusableChilds().filter(':last').focus();
});

$('.focusKeeper:last').on('focus', function(event) {
    event.preventDefault();
    $('.dialog').getFocusableChilds().filter(':first').focus();
});

May be I'll add a fiddle later, no more time for now. :(


EDIT: As KingKing noted below the focus is lost, when clicking outside the control. This may be covered by adding an mousedown handler for the .overlay:

$('.overlay').on('mousedown', function(event) {
    event.preventDefault();
    event.stopImmediatePropagation();
});

EDIT #2: There's another thing missing: Going outside the document with the focus (for example the titlebar) and than tabbing back. So we need another handler for document which puts back the focus on the first focusable element:

$(document).on('focus', function(event) {
    event.preventDefault();
    $('.dialog').getFocusableChilds().filter(':first').focus();
});
Community
  • 1
  • 1
SuperNova
  • 2,792
  • 2
  • 25
  • 34
  • Note that jQueryUI has ":tabbable" selector which selects all such elements. http://api.jqueryui.com/tabbable-selector/ – CookieEater Feb 24 '15 at 19:48
  • That's true. But it will only work when using jqueryui. I would not include the whole lib, to have the :tabbable selector. But I think it might be a good idea to take a look into their sources (may be I've missed a tabbable element). – SuperNova Feb 25 '15 at 10:14
  • @SuperNove, I know what you mean. I added the comment for others who may be already using jQuery UI. – CookieEater Feb 26 '15 at 10:51
  • Was about to post that this didn't work, but I had placed the `.focuskeeper` links *inside* the dialog div. Wasted a stinking hour on that. D'oh! Anyway, works great – Stephen R Jul 16 '19 at 14:57
3

You can try handling the focusout event for the .dialog element, check the e.target. Note about the e.relatedTarget here, it refers to the element which receives focus while e.target refers to the element lossing focus:

var tabbingForward = true;
//The body should have at least 2 input fields outside of the dialog to trap focusing,
//otherwise  focusing may be outside of the document 
//and we will loss control in such a case.
//So we create 2 dummy text fields with width = 0 (or opacity = 0)
var dummy = "<input style='width:0; opacity:0'/>";
var clickedOutside = false;
$('body').append(dummy).prepend(dummy);
$('.dialog').focusout(function(e){     
  if(clickedOutside) { 
    e.target.focus();
    clickedOutside = false;
  }
  else if(!e.relatedTarget||!$('.dialog').has(e.relatedTarget).length) {   
    var inputs = $('.dialog :input');
    var input = tabbingForward ? inputs.first() : inputs.last();
    input.focus();        
  }
}); 
$('.dialog').keydown(function(e){
  if(e.which == 9) {
    tabbingForward = !e.shiftKey;
  }
});
$('body').mousedown(function(e){
  if(!$('.dialog').has(e.target).length) {        
    clickedOutside = true;        
  }
});

Demo.

King King
  • 61,710
  • 16
  • 105
  • 130
  • Sorry, but with TAB I can focus on the link and the text field. – Pablo Lozano Jun 05 '14 at 09:58
  • Hmm, it makes something. But it's far away from the jQueryUI demo i posted above. I think there's something missing. :( – SuperNova Jun 05 '14 at 10:19
  • 1
    @SuperNova looks like it's not an easy to solve problem, we need even to use the ***dummy*** technique, try my updated demo to see if it works now. – King King Jun 05 '14 at 11:43
  • 1
    @KingKing: I really appreciate your assistance. I just saw you tried the same technique I've used know. But I think my event listeners are a little bit easier to maintain and understand. I hope it's okay, when marking my own answer as the accepted answer? – SuperNova Jun 05 '14 at 12:52
  • 1
    @SuperNova yes, that way of handling event may be easy to understand to some ones, however if you click outside your dialog, the currently focused element (which is not `:first` or `:last`) will be lost focus, if that behavior is fine, you can just use your way and of course accepting your own answer is OK if that's exactly what you want (see it as the best answer). BTW with the code you posted, I'm not sure if it works, just create the demo yourself. – King King Jun 05 '14 at 12:59
  • @SuperNova anyway I think we have to detect the ***direction*** of focusing using tab by listening to `keydown` for the tab key (like as I do in my code). – King King Jun 05 '14 at 13:04
  • @KingKing You're right, when clicking outside (on the overlay), the dialog looses the focus. When pressing TAB now, the LAST control gets focus. This is because the first ".focusKeeper" is getting the focus now and redirecting it to the last focusable control. I've changed my answer now to dead with this behaviour. – SuperNova Jun 05 '14 at 13:31