Skip to content

Instantly share code, notes, and snippets.

@leereamsnyder
Last active September 10, 2015 20:52
Show Gist options
  • Save leereamsnyder/e0da802bfc74d86bbc10 to your computer and use it in GitHub Desktop.
Save leereamsnyder/e0da802bfc74d86bbc10 to your computer and use it in GitHub Desktop.
A little jQuery plugin that I've found helps a bunch when managing focus. Adds ':focusable' and ':tabbable' selectors (from jQuery UI) and adds chainable methods to set and manage focus amongst sets of elements
;(function($, window, document, undefined){
/* :focusable and :tabbable selectors from
https://raw.github.com/jquery/jquery-ui/master/ui/jquery.ui.core.js */
function visible(element) {
return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function () {
return $.css(this, "visibility") === "hidden";
}).length;
}
function focusable(element, isTabIndexNotNaN) {
var map, mapName, img,
nodeName = element.nodeName.toLowerCase();
if ("area" === nodeName) {
map = element.parentNode;
mapName = map.name;
if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") {
return false;
}
img = $("img[usemap=#" + mapName + "]")[0];
return !!img && visible(img);
}
return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled :
"a" === nodeName ?
element.href || isTabIndexNotNaN :
isTabIndexNotNaN) &&
// the element and all of its ancestors must be visible
visible(element);
}
$.extend($.expr[":"], {
data: $.expr.createPseudo ? $.expr.createPseudo(function (dataName) {
return function (elem) {
return !!$.data(elem, dataName);
};
}) : // support: jQuery <1.8
function (elem, i, match) {
return !!$.data(elem, match[3]);
},
focusable: function (element) {
return focusable(element, !isNaN($.attr(element, "tabindex")));
},
tabbable: function (element) {
var tabIndex = $.attr(element, "tabindex"),
isTabIndexNaN = isNaN(tabIndex);
return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN);
}
});
/*
$.fn.attemptFocus
Will attempt to focus on the first :focusable element in the collection.
Solves a couple boilerplate code issues:
- filters down to focusable elements automatically
- if you end up passing no elements, no error
- if you pass multiple elements, it will only do the first instead of all of them
- if for some reason .focus() doesn't work, no error
Returns the original jQuery collection
RETURNS
-------------------
Returns the original collection
EXAMPLE
---------------------
$('a').attemptFocus();
*/
$.fn.attemptFocus = function(){
this.filter(':focusable').first().each(function(){
try { this.focus(); } catch(err) {}
});
return this;
};
/*
$.fn.traverse
Little utility function
Given a collection of elements, finds the previous or next element within that collection
Note this is NOT the same as $.prev() or $.next()! Those return sibling DOM elements
Solves a couple of boilerplate issues:
- don't have to figure out the index of an element, or if it's even in a collection
- don't have to worry about negative indexes or an index > length of the collection
Big note: It wraps around the selection!
+ If current is the last element in the collection, "next" returns the first
+ If current is the first element in the collection, "prev" returns the last element
+ If current is not in the collection, you'll get the 1st element ("next") or last ("prev")
RETURNS
-------------------
Returns a single element from the jQuery collection
PARAMETERS
----------------
dir String 'prev(ious)' or 'next'
current DOM or jQuery The 'current' element to traverse from
EXAMPLE
---------------------
$(':focusable').traverse('next',document.activeElement) // returns next focusable element after the current one
*/
$.fn.traverse = function(dir,current) {
if (typeof dir === 'string' && /prev|next/.test(dir)) {
var currentIndex = this.index(current);
// in case current is not in the collection, prev will go to the last
// next will go to the first element
var newIndex = /prev/.test(dir) ? -1 : 0;
// if current IS in the collection
if (current && currentIndex !== -1) {
newIndex = /prev/.test(dir) ? currentIndex-1 : currentIndex+1;
}
// negative indexes are OK; they just wrap around backwards
// but too large is bad. go to the first
if ( newIndex === this.length ) { newIndex = 0; }
return this.eq(newIndex);
}
return this;
};
/*
$.fn.traverseAndFocus
Another little utility to traverse a collection of elements and then attempt to focus on the new target
Pass two arguments: direction and the base index element
Will attempt to focus on the prev/next element in the collection FROM the current element you pass.
RETURNS
-------------------
As this uses $.fn.traverse, it will narrow down the jQuery collection to a single element
PARAMETERS
-------------------
dir String 'prev(ious)' or 'next'
current DOM or jQuery The 'current' element to traverse from.
Defaults to document.activeElement
EXAMPLE
---------------------
$('.menu').find(':focusable').traverseAndFocus('previous');
// will focus on the previous :focusable element from the current focused element
*/
$.fn.traverseAndFocus = function(dir,current) {
var collection = this;
if (typeof dir === 'string' && /prev|next/.test(dir)) {
collection = this.traverse(dir,current || document.activeElement).attemptFocus();
}
return collection;
};
/*
$.fn.trapFocus
Given an element, this will "trap" focus within that element.
Probably the most common usage would be modal dialogs.
If you're on the last focusable element and TAB, you will wrap around to the first focusable child element.
Likewise if you're on the first focusable element and SHIFT+TAB, you will wrap around (backwards) to the last focusable child element.
USE CAREFULLY!!!!!
You can easily keep people from being able to navigate around the page with their keyboards with this.
To undo, use $.fn.untrapFocus
RETURNS
-------------------
Returns the original jQuery collection
EXAMPLE
---------------------
$('.modal').trapFocus();
*/
$.fn.trapFocus = function() {
if (this.data('trapFocus')) { return this; }
return this.data('trapFocus', true)
.on('keydown.trapFocus', function(e){
// "tab" only
if (e.keyCode !== 9) {return;}
var $target = $(e.target);
// should calculate this every time because elements might be added or removed after initializing
var $focusables = $(this).find(':focusable');
// last focusable element, NOT with SHIFT
if ( ! e.shiftKey && $target.is( $focusables.last() ) ) {
e.preventDefault();
$focusables.traverseAndFocus('next');
}
// first focusable element, SHIFT + Tab
if ( e.shiftKey && $target.is( $focusables.first() ) ) {
e.preventDefault();
$focusables.traverseAndFocus('prev');
}
});
};
/*
$.fn.untrapFocus
Disables $.fn.trapFocus
RETURNS
-------------------
Returns the original jQuery collection
EXAMPLE
---------------------
// eg with Bootstrap modals
$('.modal')
.trapFocus()
.on('hidden', function(){ $(this).untrapFocus(); });
*/
$.fn.untrapFocus = function() {
return this.data('trapFocus', false).off('keydown.trapFocus')
};
})(jQuery, this, this.document);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment