Skip to content

Commit

Permalink
New accessibility features for Dropdown
Browse files Browse the repository at this point in the history
* up/down arrow keys menu selection
* Esc key now closes last open nested dropdown menu
* While the dropdown is open, pressing up/down arrows the component will prevent the default scroll behavior

Also fixing #169
  • Loading branch information
thednp committed Nov 20, 2017
1 parent ba85ce0 commit 1658da4
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 103 deletions.
56 changes: 41 additions & 15 deletions dist/bootstrap-native-v4.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Native Javascript for Bootstrap 4 v2.0.19 | © dnp_theme | MIT-License
// Native Javascript for Bootstrap 4 v2.0.20 | © dnp_theme | MIT-License
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD support:
Expand Down Expand Up @@ -90,6 +90,7 @@
clickEvent = 'click',
hoverEvent = 'hover',
keydownEvent = 'keydown',
keyupEvent = 'keyup',
resizeEvent = 'resize',
scrollEvent = 'scroll',
// originalEvents
Expand Down Expand Up @@ -471,7 +472,7 @@
this[keyboard] = options[keyboard] === true || keyboardData;
this[pause] = (options[pause] === hoverEvent || pauseData) ? hoverEvent : false; // false / hover

if ( !( options[interval] || intervalData ) ) { // determine slide interval
if ( !options[interval] || !intervalData ) { // determine slide interval
this[interval] = false;
} else {
this[interval] = parseInt(options[interval]) || intervalData; // default slide interval
Expand Down Expand Up @@ -675,7 +676,6 @@
// set options
options = options || {};


// event targets and constants
var accordion = null, collapse = null, self = this,
isAnimating = false, // when true it will prevent click handlers
Expand Down Expand Up @@ -785,46 +785,68 @@
this.persist = option === true || element[getAttribute]('data-persist') === 'true' || false;

// constants, event targets, strings
var self = this,
var self = this, tabindex = 'tabindex', children = 'children',
parent = element[parentNode],
component = 'dropdown', open = 'open',
relatedTarget = null,
menu = queryElement('.dropdown-menu', parent),
menuItems = (function(){
var set = menu[children], newSet = []; console.log(set)
for ( var i=0; i<set[length]; i++ ){

set[i].tagName === 'A' && newSet.push(set[i]);
set[i][children][length] && (set[i][children][0].tagName === 'A' && newSet.push(set[i][children][0]));
}
return newSet;
})(),

// preventDefault on empty anchor links
preventEmptyAnchor = function(anchor){
(/\#$/.test(anchor.href) || anchor[parentNode] && /\#$/.test(anchor[parentNode].href)) && this[preventDefault](); // should be here to prevent jumps
(/\#$/.test(anchor.href) || anchor[parentNode] && /\#$/.test(anchor[parentNode].href))
&& this[preventDefault]();
},

// toggle dismissible events
toggleDismiss = function(){
var type = element[open] ? on : off;
type(DOC, keydownEvent, keyHandler);
type(DOC, clickEvent, dismissHandler);
type(DOC, keydownEvent, preventScroll);
type(DOC, keyupEvent, keyHandler);
},

// handlers
dismissHandler = function(e) {
var eventTarget = e[target],
hasData = eventTarget && (eventTarget[getAttribute](dataToggle)
|| eventTarget[parentNode] && getAttribute in eventTarget[parentNode]
&& eventTarget[parentNode][getAttribute](dataToggle));

var eventTarget = e[target], hasData = eventTarget && stringDropdown in eventTarget;
if ( (eventTarget === menu || menu.contains(eventTarget)) && (self.persist || hasData) ) { return; }
else {
relatedTarget = eventTarget === element || element.contains(eventTarget) ? element : null;
hide();
}
preventEmptyAnchor.call(e,eventTarget);
},
keyHandler = function(e) {
if ( element[open] && e.which === 27 ) { relatedTarget = null; hide(); }
},
clickHandler = function(e) {
relatedTarget = element;
show();
preventEmptyAnchor.call(e,e[target]);
},
preventScroll = function(e){
var key = e.which || e.keyCode;
if( key === 38 || key === 40 ) { e[preventDefault](); }
},
keyHandler = function(e){
var eventTarget = e[target], key = e.which || e.keyCode, activeItem = DOC.activeElement,
idx = menuItems[indexOf](activeItem);

if ( activeItem[parentNode][parentNode] === menu || activeItem[parentNode] === menu || activeItem === menu) { // navigate up | down
idx = key === 38 ? (idx>1?idx-1:0) : key === 40 ? (idx<menuItems[length]-1?idx+1:idx) : idx;
setFocus(menuItems[idx]);
}

if ( (activeItem[parentNode][parentNode] === menu || activeItem[parentNode] === menu && element[open]) && key === 27 ) { // dismiss on ESC
self.toggle();
relatedTarget = null;
}
},

// private methods
show = function() {
Expand All @@ -835,6 +857,9 @@
bootstrapCustomEvent.call(parent, shownEvent, component, relatedTarget);
element[open] = true;
off(element, clickEvent, clickHandler);
// focus the first menu item | menu
menu[getElementsByTagName]('A')[length] ? setFocus( menu[getElementsByTagName]('A')[0] )
: setFocus(menu);
setTimeout(function(){ toggleDismiss(); },1);
},
hide = function() {
Expand All @@ -845,6 +870,7 @@
bootstrapCustomEvent.call(parent, hiddenEvent, component, relatedTarget);
element[open] = false;
toggleDismiss();
setFocus(element);
setTimeout(function(){ on(element, clickEvent, clickHandler); },1);
};

Expand All @@ -859,7 +885,7 @@

// init
if ( !(stringDropdown in element) ) { // prevent adding event handlers twice
menu[setAttribute]('tabindex', '0'); // Fix onblur on Chrome | Safari
!tabindex in menu && menu[setAttribute]('tabindex', '0'); // Fix onblur on Chrome | Safari
on(element, clickEvent, clickHandler);
}

Expand Down
4 changes: 2 additions & 2 deletions dist/bootstrap-native-v4.min.js

Large diffs are not rendered by default.

61 changes: 43 additions & 18 deletions dist/bootstrap-native.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Native Javascript for Bootstrap 3 v2.0.19 | © dnp_theme | MIT-License
// Native Javascript for Bootstrap 3 v2.0.20 | © dnp_theme | MIT-License
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD support:
Expand Down Expand Up @@ -92,6 +92,7 @@
clickEvent = 'click',
hoverEvent = 'hover',
keydownEvent = 'keydown',
keyupEvent = 'keyup',
resizeEvent = 'resize',
scrollEvent = 'scroll',
// originalEvents
Expand Down Expand Up @@ -627,7 +628,7 @@
this[keyboard] = options[keyboard] === true || keyboardData;
this[pause] = (options[pause] === hoverEvent || pauseData) ? hoverEvent : false; // false / hover

if ( !( options[interval] || intervalData ) ) { // determine slide interval
if ( !options[interval] || !intervalData ) { // determine slide interval
this[interval] = false;
} else {
this[interval] = parseInt(options[interval]) || intervalData; // default slide interval
Expand Down Expand Up @@ -830,7 +831,6 @@
// set options
options = options || {};


// event targets and constants
var accordion = null, collapse = null, self = this,
isAnimating = false, // when true it will prevent click handlers
Expand Down Expand Up @@ -940,46 +940,67 @@
this.persist = option === true || element[getAttribute]('data-persist') === 'true' || false;

// constants, event targets, strings
var self = this,
var self = this, tabindex = 'tabindex', children = 'children',
parent = element[parentNode],
component = 'dropdown', open = 'open',
relatedTarget = null,
menu = queryElement('.dropdown-menu', parent),
menuItems = (function(){
var set = nodeListToArray(menu[children]);
for ( var i=0; i<set[length]; i++ ){
if ( !set[i][children][length] ) set.splice(i,1);
}
return set;
})(),

// preventDefault on empty anchor links
preventEmptyAnchor = function(anchor){
(/\#$/.test(anchor.href) || anchor[parentNode] && /\#$/.test(anchor[parentNode].href)) && this[preventDefault](); // should be here to prevent jumps
(/\#$/.test(anchor.href) || anchor[parentNode] && /\#$/.test(anchor[parentNode].href))
&& this[preventDefault]();
},

// toggle dismissible events
toggleDismiss = function(){
var type = element[open] ? on : off;
type(DOC, keydownEvent, keyHandler);
type(DOC, clickEvent, dismissHandler);
type(DOC, keydownEvent, preventScroll);
type(DOC, keyupEvent, keyHandler);
},

// handlers
dismissHandler = function(e) {
var eventTarget = e[target],
hasData = eventTarget && (eventTarget[getAttribute](dataToggle)
|| eventTarget[parentNode] && getAttribute in eventTarget[parentNode]
&& eventTarget[parentNode][getAttribute](dataToggle));

var eventTarget = e[target], hasData = eventTarget && stringDropdown in eventTarget;
if ( (eventTarget === menu || menu.contains(eventTarget)) && (self.persist || hasData) ) { return; }
else {
relatedTarget = eventTarget === element || element.contains(eventTarget) ? element : null;
hide();
}
preventEmptyAnchor.call(e,eventTarget);
},
keyHandler = function(e) {
if (element[open] && (e.which === 27 || e.keyCode === 27)) { relatedTarget = null; hide(); } // e.keyCode for IE8
},
clickHandler = function(e) {
relatedTarget = element;
show();
preventEmptyAnchor.call(e,e[target]);
},
preventScroll = function(e){
var key = e.which || e.keyCode;
if( key === 38 || key === 40 ) { e[preventDefault](); }
},
keyHandler = function(e){
var eventTarget = e[target], key = e.which || e.keyCode,
activeItem = DOC.activeElement,
idx = menuItems[indexOf](activeItem[parentNode]);

if ( activeItem[parentNode][parentNode] === menu || activeItem === menu) { // navigate up | down
idx = key === 38 ? (idx>1?idx-1:0) : key === 40 ? (idx<menuItems[length]-1?idx+1:idx) : idx;
setFocus(menuItems[idx][children][0]);
}

if ( activeItem[parentNode][parentNode] === menu && element[open] && key === 27 ) { // dismiss on ESC
self.toggle();
relatedTarget = null;
}
},

// private methods
show = function() {
Expand All @@ -989,6 +1010,9 @@
bootstrapCustomEvent.call(parent, shownEvent, component, relatedTarget);
element[open] = true;
off(element, clickEvent, clickHandler);
// focus the first menu item | menu
menu[getElementsByTagName]('A')[length] ? setFocus( menu[getElementsByTagName]('A')[0] )
: setFocus(menu);
setTimeout(function(){ toggleDismiss(); },1);
},
hide = function() {
Expand All @@ -998,11 +1022,12 @@
bootstrapCustomEvent.call(parent, hiddenEvent, component, relatedTarget);
element[open] = false;
toggleDismiss();
setFocus(element);
setTimeout(function(){ on(element, clickEvent, clickHandler); },1);
};

// set initial state to closed
element[open] = false;
// set initial state to closed
element[open] = false;

// public methods
this.toggle = function() {
Expand All @@ -1011,8 +1036,8 @@
};

// init
if ( !(stringDropdown in element) ) { // prevent adding event handlers twice
menu[setAttribute]('tabindex', '0'); // Fix onblur on Chrome | Safari
if (!(stringDropdown in element)) { // prevent adding event handlers twice
!tabindex in menu && menu[setAttribute](tabindex, '0'); // Fix onblur on Chrome | Safari
on(element, clickEvent, clickHandler);
}

Expand Down
4 changes: 2 additions & 2 deletions dist/bootstrap-native.min.js

Large diffs are not rendered by default.

0 comments on commit 1658da4

Please sign in to comment.