diff --git a/src/modal/docs/demo.html b/src/modal/docs/demo.html
index 48302c7065..4109628073 100644
--- a/src/modal/docs/demo.html
+++ b/src/modal/docs/demo.html
@@ -6,7 +6,7 @@
Selected:
{{ selected.item }}
diff --git a/src/modal/modal.js b/src/modal/modal.js
index 7d91190e5c..821ba32c36 100644
--- a/src/modal/modal.js
+++ b/src/modal/modal.js
@@ -196,6 +196,13 @@ angular.module('ui.bootstrap.modal', [])
NOW_CLOSING_EVENT: 'modal.stack.now-closing'
};
+ //Modal focus behavior
+ var focusableElementList;
+ var focusIndex = 0;
+ var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' +
+ 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' +
+ 'iframe, object, embed, *[tabindex], *[contenteditable=true]';
+
function backdropIndex() {
var topBackdropIndex = -1;
var opened = openedWindows.keys();
@@ -281,15 +288,35 @@ angular.module('ui.bootstrap.modal', [])
}
$document.bind('keydown', function (evt) {
- var modal;
-
- if (evt.which === 27) {
- modal = openedWindows.top();
- if (modal && modal.value.keyboard) {
- evt.preventDefault();
- $rootScope.$apply(function () {
- $modalStack.dismiss(modal.key, 'escape key press');
- });
+ var modal = openedWindows.top();
+ if (modal && modal.value.keyboard) {
+ switch (evt.which){
+ case 27:{
+ evt.preventDefault();
+ $rootScope.$apply(function () {
+ $modalStack.dismiss(modal.key, 'escape key press');
+ });
+ break;
+ }
+ case 9:{
+ $modalStack.loadFocusElementList(modal);
+ var focusChanged = false;
+ if (evt.shiftKey) {
+ if($modalStack.isFocusInFirstItem(evt)){
+ focusChanged = $modalStack.focusLastFocusableElement();
+ }
+ }
+ else{
+ if($modalStack.isFocusInLastItem(evt)){
+ focusChanged = $modalStack.focusFirstFocusableElement();
+ }
+ }
+ if(focusChanged){
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ break;
+ }
}
}
});
@@ -338,6 +365,7 @@ angular.module('ui.bootstrap.modal', [])
openedWindows.top().value.modalOpener = modalOpener;
body.append(modalDomEl);
body.addClass(OPENED_MODAL_CLASS);
+ $modalStack.clearFocusListCache();
};
function broadcastClosing(modalWindow, resultOrReason, closing) {
@@ -382,6 +410,51 @@ angular.module('ui.bootstrap.modal', [])
}
};
+ $modalStack.focusFirstFocusableElement = function(){
+ if(focusableElementList.length > 0){
+ focusableElementList[0].focus();
+ return true;
+ }
+ return false;
+ };
+ $modalStack.focusLastFocusableElement = function(){
+ if(focusableElementList.length > 0) {
+ focusableElementList[focusableElementList.length-1].focus();
+ return true;
+ }
+ return false;
+ };
+
+ $modalStack.isFocusInFirstItem = function(evt){
+ if(focusableElementList.length > 0){
+ return (evt.target || evt.srcElement) == focusableElementList[0];
+ }
+ return false;
+ };
+
+ $modalStack.isFocusInLastItem = function(evt){
+ if(focusableElementList.length > 0){
+ return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length-1];
+ }
+ return false;
+ };
+
+ $modalStack.clearFocusListCache = function(){
+ focusableElementList = [];
+ focusIndex = 0;
+ };
+
+ $modalStack.loadFocusElementList = function (modalWindow){
+ if(focusableElementList === undefined || focusableElementList.length === 0){
+ if(modalWindow){
+ var modalDomE1 = modalWindow.value.modalDomEl;
+ if(modalDomE1 && modalDomE1.length > 0) {
+ focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector);
+ }
+ }
+ }
+ };
+
return $modalStack;
}])
diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js
index 4d9ea9d699..c97ffb1bbf 100644
--- a/src/modal/test/modal.spec.js
+++ b/src/modal/test/modal.spec.js
@@ -2,9 +2,11 @@ describe('$modal', function () {
var $controllerProvider, $rootScope, $document, $compile, $templateCache, $timeout, $q;
var $modal, $modalProvider;
- var triggerKeyDown = function (element, keyCode) {
+ var triggerKeyDown = function (element, keyCode, shiftKey) {
var e = $.Event('keydown');
+ e.srcElement = element[0];
e.which = keyCode;
+ e.shiftKey = shiftKey;
element.trigger(e);
};
@@ -340,6 +342,40 @@ describe('$modal', function () {
openAndCloseModalWithAutofocusElement();
openAndCloseModalWithAutofocusElement();
});
+
+ it('should change focus to first element when tab key was pressed', function() {
+ var initialPage = angular.element('
Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'
' +
+ ''
+ });
+ expect($document).toHaveModalsOpen(1);
+
+ var lastElement = angular.element(document.getElementById('tab-focus-button'));
+ lastElement.focus();
+ triggerKeyDown(lastElement, 9);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-link');
+ });
+
+ it('should change focus to last element when shift+tab key is pressed', function() {
+ var initialPage = angular.element('Outland link');
+ angular.element(document.body).append(initialPage);
+ initialPage.focus();
+
+ open({
+ template:'
' +
+ ''
+ });
+ expect($document).toHaveModalsOpen(1);
+
+ var lastElement = angular.element(document.getElementById('tab-focus-link'));
+ lastElement.focus();
+ triggerKeyDown(lastElement, 9, true);
+ expect(document.activeElement.getAttribute('id')).toBe('tab-focus-button');
+ });
});
describe('default options can be changed in a provider', function () {