Last active
September 10, 2015 20:52
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(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