├── .gitignore ├── README.md ├── bower.json ├── dist ├── a11y_kit.jquery.js └── a11y_kit.js ├── package.json └── src ├── _access.js ├── _announce.js ├── _hide_show_from_AT.js └── a11y_kit.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A11yKit 2 | 3 | ## Essential JS tools that empower modern accessibility 4 | This library provides JavaScript methods that are useful in managing user focus, announcing content and selecting interactive elements. 5 | 6 | A11yKit features: 7 | - [`access()`](#access) - focus on anything in the DOM without using `tabindex` 8 | 9 | 10 | 11 | ## Installation 12 | 13 | ### Bower Package 14 | 15 | ``` 16 | bower install a11y_kit 17 | ``` 18 | 19 | In your page, link to the file at: 20 | 21 | ``` 22 | bower_components/dist/a11y_kit.jquery.js 23 | ``` 24 | 25 | 26 | ## Access - Focus on anything 27 | ### Usage 28 | ``` 29 | $(selector).access(place_focus_before) 30 | ``` 31 | 32 | ### Params 33 | - ```@place_focus_before: boolean(default: false)``` if true, focus is placed on a temporary span tag inserted before the specified element. By default, focus is placed on the specified element itself. 34 | 35 | ### Why use $.access()? 36 | Managing focus on web pages can be complex, and requires the use of tabindex. The tabindex attribute allows any element to receive focus, but hard coding tabindex into your pages makes your project more brittle and error-prone. _One example:_ using tabindex="0" on a container makes all child interactive elements read the container's entire contents(in VoiceOver). 37 | 38 | A far more elegant solution is to place focus on an element dynamically using a script like $.access(). 39 | 40 | __General rule:__ don't hard code tabindex - instead, use $.access() to manage focus. 41 | 42 | ###How It Works 43 | This method allows focus of elements that do not natively support .focus(). This is accomplished via the addition of tabindex="-1" to the supplied target and allows it to temporarily receive focus. Once the element is blurred, everything is cleaned up and returned to its original state. 44 | 45 | ### Behavior 46 | When focus is placed on a container, screen readers may either 1) read the contents of the container or 2) read any associated label(e.g. aria-label, aria-labelledby) on the element. 47 | 48 | ## Announce - Say anything 49 | 50 | ### Usage 51 | ``` 52 | $.announce(message, manner) 53 | ``` 54 | 55 | __Note:__ Requires a dedicated #a11y_announcer container with a hard-coded aria-live attribute that stays in the page at all times. _This element cannot be created dynamically and must be in the page on page load._ 56 | 57 | ### Example 58 | ``` 59 |
60 | ``` 61 | 62 | ### Params 63 | - ``` @message: string``` copy/message to be announced 64 | - ```@manner: ['polite'(default), 'assertive']``` manner is which message is announced 65 | 66 | 67 | Announce content via a dedicated, global aria-live announcement container. announce() works by simply updating the content of the #a11y_container with the content to be spoken. It also performs a reset of sorts by toggling the @aria-live value to 'off', clearing the contents, and lastly resetting the @aria-live value to its original value. This allows for repeated messages to be spoken, if needed. 68 | 69 | 70 | ### Why use $.announce()? 71 | The promise of live regions and aria-live is to improve understanding and perception of dynamic content. In reality, though, overuse of live regions can create a lot more problems than it solves. Too many live regions on a page makes it more difficult to debug related issues. 72 | 73 | At any given time, only one live region can speak its content, so there's never a need to have more than one in the page. Using a single, common live region and the $.announce() script greatly simplifies your code and debugging efforts. 74 | 75 | __General Rule:__ Do not use _aria-live_ or any live region role( _role=alert|log|marquee|status|timer_) - instead, use ONE common live region and $.announce() your content as needed. 76 | 77 | ## Utility pseudo-selectors for jQuery 78 | Select all :focusable and :tabbable elements. Credit: jQuery UI 79 | 80 | ### Usage 81 | ``` 82 | $(':focusable') # -> returns all focusable elements 83 | 84 | $(':tabbable') # -> returns all tabbable elements 85 | ``` 86 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a11yfox", 3 | "version": "0.0.4", 4 | "authors": [ 5 | "Patrick Fox" 6 | ], 7 | "description": "A set of utilities that empower modern accessibility.", 8 | "main": "dist/a11yfox.js", 9 | "moduleType": [ 10 | "amd" 11 | ], 12 | "keywords": [ 13 | "accessibility", 14 | "a11y", 15 | "announce", 16 | "access", 17 | "focus management", 18 | "live", 19 | "regions", 20 | "aria-live" 21 | ], 22 | "license": "MIT", 23 | "homepage": "TBD", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test", 29 | "tests", 30 | "*.md", 31 | "package.json", 32 | "src", 33 | "Gulpfile.js" 34 | ], 35 | "dependencies": { 36 | "jquery": "~1.10" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dist/a11y_kit.jquery.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var announce_timeout, focusable, visible; 3 | 4 | $.fn.access = function(place_focus_before) { 5 | var ogti, target, temp_em; 6 | if (place_focus_before) { 7 | temp_em = $(''); 8 | temp_em.insertBefore(this); 9 | temp_em.attr('tabindex', '-1').on('blur focusout', function() { 10 | return $(this).remove(); 11 | }).focus(); 12 | } else { 13 | ogti = 'original-tabindex'; 14 | target = $(this); 15 | target.data(ogti, target.attr('tabindex') || false); 16 | target.attr('tabindex', '-1').on('blur focusout', function() { 17 | if (target.data(ogti) !== false) { 18 | return target.attr('tabindex', target.data(ogti)); 19 | } else { 20 | target.removeAttr('tabindex'); 21 | target.off('blur focusout'); 22 | return target.data(ogti, false); 23 | } 24 | }).focus(); 25 | } 26 | return this; 27 | }; 28 | 29 | announce_timeout = null; 30 | 31 | $.announce = function(message, manner) { 32 | var announcer, clear_announcer; 33 | manner = manner || 'polite'; 34 | announcer = $('#a11y_announcer').attr('aria-live', 'off'); 35 | clear_announcer = function() { 36 | announcer.html(''); 37 | announce_timeout = null; 38 | }; 39 | announcer.attr('aria-live', manner); 40 | announcer.html(message); 41 | clearTimeout(announce_timeout); 42 | announce_timeout = setTimeout(clear_announcer, 500); 43 | return this; 44 | }; 45 | 46 | visible = function(element) { 47 | return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function() { 48 | return $.css(this, 'visibility') === 'hidden'; 49 | }).length; 50 | }; 51 | 52 | focusable = function(element, isTabIndexNotNaN) { 53 | var img, map, mapName, nodeName; 54 | nodeName = element.nodeName.toLowerCase(); 55 | if ('area' === nodeName) { 56 | map = element.parentNode; 57 | mapName = map.name; 58 | if (!element.href || !mapName || map.nodeName.toLowerCase() !== 'map') { 59 | return false; 60 | } 61 | img = $('img[usemap=#' + mapName + ']')[0]; 62 | return !!img && visible(img); 63 | } 64 | return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled : 'a' === nodeName ? element.href || isTabIndexNotNaN : isTabIndexNotNaN) && visible(element); 65 | }; 66 | 67 | $.extend($.expr[':'], { 68 | data: ($.expr.createPseudo ? $.expr.createPseudo(function(dataName) { 69 | return function(elem) { 70 | return !!$.data(elem, dataName); 71 | }; 72 | }) : function(elem, i, match) { 73 | return !!$.data(elem, match[3]); 74 | }), 75 | focusable: function(element) { 76 | return focusable(element, !isNaN($.attr(element, "tabindex"))); 77 | }, 78 | tabbable: function(element) { 79 | var isTabIndexNaN, tabIndex; 80 | tabIndex = $.attr(element, "tabindex"); 81 | isTabIndexNaN = isNaN(tabIndex); 82 | return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 83 | } 84 | }); 85 | 86 | }).call(this); 87 | -------------------------------------------------------------------------------- /dist/a11y_kit.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | (function(ns) { 3 | var access, announce, announce_timeout; 4 | announce_timeout = void 0; 5 | access = function(el, place_focus_before) { 6 | temp_el; 7 | var focus_el, focus_method, ogti, onblur_el, onblur_temp_el, temp_el; 8 | focus_el = void 0; 9 | focus_method = void 0; 10 | ogti = void 0; 11 | onblur_el = void 0; 12 | onblur_temp_el = void 0; 13 | temp_el = void 0; 14 | onblur_el = function(e) { 15 | if (el.getAttribute('data-ogti')) { 16 | el.setAttribute('tabindex', ogti); 17 | } else { 18 | el.removeAttribute('tabindex'); 19 | } 20 | el.removeAttribute('data-ogti'); 21 | el.removeEventListener('focusout', focus_method); 22 | }; 23 | onblur_temp_el = function(e) { 24 | temp_el.removeEventListener('focusout', focus_method); 25 | temp_el.parentNode.removeChild(temp_el); 26 | }; 27 | focus_el = function(the_el) { 28 | the_el.setAttribute('tabindex', '-1'); 29 | the_el.addEventListener('focusout', focus_method); 30 | the_el.focus(); 31 | }; 32 | focus_method = onblur_el; 33 | if (place_focus_before) { 34 | temp_el = document.createElement('span'); 35 | if (typeof place_focus_before === 'string') { 36 | temp_el.innerHTML = place_focus_before; 37 | } 38 | temp_el.setAttribute('style', 'position: absolute;height: 1px;width: 1px;margin: -1px;padding: 0;overflow: hidden;clip: rect(0 0 0 0);border: 0;'); 39 | temp_el = el.parentNode.insertBefore(temp_el, el); 40 | focus_method = onblur_temp_el; 41 | focus_el(temp_el); 42 | } else { 43 | ogti = el.getAttribute('tabindex'); 44 | if (ogti) { 45 | el.setAttribute('data-ogti', ogti); 46 | } 47 | focus_el(el); 48 | } 49 | }; 50 | window.access = access; 51 | announce_timeout = null; 52 | announce = function(message, manners) { 53 | var announcer, clear_announcer; 54 | manners = manners || 'polite'; 55 | announcer = document.getElementByID('a11y_announcer'); 56 | announcer.setAttribute('aria-live', 'off'); 57 | clear_announcer = function() { 58 | announcer.innerHTML = ''; 59 | announce_timeout = null; 60 | return announcer; 61 | }; 62 | clear_announcer.setAttribute('aria-live', manners); 63 | announcer.innerHTML = message; 64 | clearTimeout(announce_timeout); 65 | announce_timeout = setTimeout(clear_announcer, 500); 66 | return announcer; 67 | }; 68 | window.announce = announce; 69 | $.extend($.expr[":"], { 70 | data: ($.expr.createPseudo ? $.expr.createPseudo(function(dataName) { 71 | return function(elem) { 72 | return !!$.data(elem, dataName); 73 | }; 74 | }) : function(elem, i, match) { 75 | return !!$.data(elem, match[3]); 76 | }), 77 | focusable: function(element) { 78 | return focusable(element, !isNaN($.attr(element, "tabindex"))); 79 | }, 80 | tabbable: function(element) { 81 | var isTabIndexNaN, tabIndex; 82 | isTabIndexNaN = void 0; 83 | tabIndex = void 0; 84 | tabIndex = $.attr(element, "tabindex"); 85 | isTabIndexNaN = isNaN(tabIndex); 86 | return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 87 | } 88 | }); 89 | }); 90 | 91 | }).call(this); 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a11y_libs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "Gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/_access.js: -------------------------------------------------------------------------------- 1 | const access = function(el, place_focus_before) { 2 | temp_el; 3 | let focus_el = undefined; 4 | let focus_method = undefined; 5 | let ogti = undefined; 6 | let onblur_el = undefined; 7 | let onblur_temp_el = undefined; 8 | var temp_el = undefined; 9 | onblur_el = function(e){ 10 | if (el.getAttribute('data-ogti')) { 11 | el.setAttribute('tabindex', ogti); 12 | } else { 13 | el.removeAttribute('tabindex'); 14 | } 15 | el.removeAttribute('data-ogti'); 16 | el.removeEventListener('focusout', focus_method); 17 | }; 18 | 19 | onblur_temp_el = function(e){ 20 | temp_el.removeEventListener('focusout', focus_method); 21 | temp_el.parentNode.removeChild(temp_el); 22 | }; 23 | 24 | focus_el = function(the_el){ 25 | the_el.setAttribute('tabindex', '-1'); 26 | the_el.addEventListener('focusout', focus_method); 27 | the_el.focus(); 28 | }; 29 | 30 | focus_method = onblur_el; 31 | if (place_focus_before) { 32 | temp_el = document.createElement('span'); 33 | if (typeof place_focus_before === 'string') { 34 | temp_el.innerHTML = place_focus_before; 35 | } 36 | temp_el.setAttribute('style', 'position: absolute;height: 1px;width: 1px;margin: -1px;padding: 0;overflow: hidden;clip: rect(0 0 0 0);border: 0;'); 37 | temp_el = el.parentNode.insertBefore(temp_el, el); 38 | focus_method = onblur_temp_el; 39 | focus_el(temp_el); 40 | } else { 41 | ogti = el.getAttribute('tabindex'); 42 | if (ogti) { el.setAttribute('data-ogti', ogti); } 43 | focus_el(el); 44 | } 45 | }; 46 | 47 | ns.access = access; -------------------------------------------------------------------------------- /src/_announce.js: -------------------------------------------------------------------------------- 1 | announce_timeout = null; 2 | let announcer = undefined; 3 | const announce = function(message, manners) { 4 | const clear_announcer = function() { 5 | announcer.innerHTML = ''; 6 | announce_timeout = null; 7 | return announcer; 8 | }; 9 | manners = manners || 'polite'; 10 | if (!announcer) { 11 | announcer = document.createElement('div'); 12 | } 13 | announcer.setAttribute('aria-live', 'off'); 14 | 15 | clear_announcer().setAttribute('aria-live', manners); 16 | announcer.innerHTML = message; 17 | clearTimeout(announce_timeout); 18 | announce_timeout = setTimeout(clear_announcer, 500); 19 | return announcer; 20 | }; 21 | 22 | ns.announce = announce; -------------------------------------------------------------------------------- /src/_hide_show_from_AT.js: -------------------------------------------------------------------------------- 1 | const hideFromAT = function(el){ 2 | const { body } = document; 3 | let currentEl = el; 4 | 5 | // If there are any nodes with oldAriaHiddenVal set, we should 6 | // bail, since it has already been done. 7 | const hiddenEl = document.querySelector(`[${this.oldAriaHiddenVal}]`); 8 | 9 | if (hiddenEl !== null) { 10 | // eslint-disable-next-line no-console 11 | console.warn('Attempted to run hideFromAT() twice in a row. unhideFromAT() must be executed before it run again.'); 12 | return; 13 | } 14 | do { 15 | const siblings = currentEl.parentNode.childNodes; 16 | for (let i = 0; i < siblings.length; i++) { 17 | const sibling = siblings[i]; 18 | if (sibling !== currentEl && sibling.setAttribute) { 19 | sibling.setAttribute(this.oldAriaHiddenVal, sibling.ariaHidden || 'null'); 20 | sibling.setAttribute('aria-hidden', 'true'); 21 | } 22 | } 23 | currentEl = currentEl.parentNode; 24 | } while (currentEl !== body); 25 | }; 26 | 27 | const unhideFromAT = function() { 28 | const elsToReset = document.querySelectorAll(`[${this.oldAriaHiddenVal}]`); 29 | 30 | for (let i = 0; i < elsToReset.length; i++) { 31 | const el = elsToReset[i]; 32 | const ariaHiddenVal = el.getAttribute(this.oldAriaHiddenVal); 33 | if (ariaHiddenVal === 'null') { 34 | el.removeAttribute('aria-hidden'); 35 | } else { 36 | el.setAttribute('aria-hidden', ariaHiddenVal); 37 | } 38 | el.removeAttribute(this.oldAriaHiddenVal); 39 | } 40 | }; -------------------------------------------------------------------------------- /src/a11y_kit.js: -------------------------------------------------------------------------------- 1 | (function(ns) { 2 | 3 | let announce_timeout = undefined; 4 | 5 | const access = function(el, place_focus_before) { 6 | temp_el; 7 | let focus_el = undefined; 8 | let focus_method = undefined; 9 | let ogti = undefined; 10 | let onblur_el = undefined; 11 | let onblur_temp_el = undefined; 12 | var temp_el = undefined; 13 | onblur_el = function(e){ 14 | if (el.getAttribute('data-ogti')) { 15 | el.setAttribute('tabindex', ogti); 16 | } else { 17 | el.removeAttribute('tabindex'); 18 | } 19 | el.removeAttribute('data-ogti'); 20 | el.removeEventListener('focusout', focus_method); 21 | }; 22 | 23 | onblur_temp_el = function(e){ 24 | temp_el.removeEventListener('focusout', focus_method); 25 | temp_el.parentNode.removeChild(temp_el); 26 | }; 27 | 28 | focus_el = function(the_el){ 29 | the_el.setAttribute('tabindex', '-1'); 30 | the_el.addEventListener('focusout', focus_method); 31 | the_el.focus(); 32 | }; 33 | 34 | focus_method = onblur_el; 35 | if (place_focus_before) { 36 | temp_el = document.createElement('span'); 37 | if (typeof place_focus_before === 'string') { 38 | temp_el.innerHTML = place_focus_before; 39 | } 40 | temp_el.setAttribute('style', 'position: absolute;height: 1px;width: 1px;margin: -1px;padding: 0;overflow: hidden;clip: rect(0 0 0 0);border: 0;'); 41 | temp_el = el.parentNode.insertBefore(temp_el, el); 42 | focus_method = onblur_temp_el; 43 | focus_el(temp_el); 44 | } else { 45 | ogti = el.getAttribute('tabindex'); 46 | if (ogti) { el.setAttribute('data-ogti', ogti); } 47 | focus_el(el); 48 | } 49 | }; 50 | 51 | ns.access = access; 52 | 53 | announce_timeout = null; 54 | let announcer = undefined; 55 | const announce = function(message, manners) { 56 | const clear_announcer = function() { 57 | announcer.innerHTML = ''; 58 | announce_timeout = null; 59 | return announcer; 60 | }; 61 | manners = manners || 'polite'; 62 | if (!announcer) { 63 | announcer = document.createElement('div'); 64 | } 65 | announcer.setAttribute('aria-live', 'off'); 66 | 67 | clear_announcer().setAttribute('aria-live', manners); 68 | announcer.innerHTML = message; 69 | clearTimeout(announce_timeout); 70 | announce_timeout = setTimeout(clear_announcer, 500); 71 | return announcer; 72 | }; 73 | 74 | ns.announce = announce; 75 | 76 | }); --------------------------------------------------------------------------------