diff --git a/src/modal/modal.js b/src/modal/modal.js index 9470f62d5c..bec1b432fe 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -147,6 +147,65 @@ angular.module('ui.bootstrap.modal', []) }; }]) + .directive('enforceFocus',['$document', '$modalStack', function ($document, $modalStack) { + return { + link: function($scope, iElm) { + var body = $document.find('body').eq(0); + var trapFocusDomEl = angular.element('
'); + body.append(trapFocusDomEl); + + + function currentModal(){ + var modal = $modalStack.getTop(); + if (modal && modal.value) { + return modal.value.modalDomEl[0] === iElm[0]; + } + } + + //enforceFocus inside modal + function enforceFocus(evt) { + if (!currentModal()) { + return; + } + if (iElm[0] !== evt.target && !iElm[0].contains(evt.target)) { + iElm[0].focus(); + } + } + $document[0].addEventListener('focus', enforceFocus, true); + + + //return lastFocusable element inside modal + function lastFocusable(domEl) { + var tababbleSelector = 'a[href], area[href], input:not([disabled]), button:not([disabled]),select:not([disabled]), textarea:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]'; + var list = domEl.querySelectorAll(tababbleSelector); + return list[list.length - 1]; + } + + var lastEl = lastFocusable(iElm[0]); + //focus lastElement when shitKey Tab first element + function shiftKeyTabTrap (evt) { + if (!currentModal()) { + return; + } + if(iElm[0] === evt.target && evt.shiftKey && evt.keyCode === 9){ + lastEl.focus(); + evt.preventDefault(); + } + } + $document.bind('keydown', shiftKeyTabTrap); + + + $scope.$on('$destroy',function() { + //Remove trap + trapFocusDomEl.remove(); + //Remove event listener + $document[0].removeEventListener('focus', enforceFocus, true); + $document.unbind('keydown', shiftKeyTabTrap); + }); + } + }; + }]) + .directive('modalAnimationClass', [ function () { return { @@ -291,7 +350,7 @@ angular.module('ui.bootstrap.modal', []) body.append(backdropDomEl); } - var angularDomEl = angular.element('
'); + var angularDomEl = angular.element('
'); angularDomEl.attr({ 'template-url': modal.windowTemplateUrl, 'window-class': modal.windowClass, diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js index c1d5fe4c29..dcc1c6e01e 100644 --- a/src/modal/test/modal.spec.js +++ b/src/modal/test/modal.spec.js @@ -272,6 +272,29 @@ describe('$modal', function () { element.remove(); }); + it('should support Tab and return focus to the dialog', function () { + var link = 'Link'; + var aElement = angular.element(link); + angular.element(document.body).append(aElement); + aElement.focus(); + expect(document.activeElement.tagName).toBe('A'); + + var modal = open({template: '
Content
'}); + $timeout.flush(); + + //Focus to last link + var lastAElement = angular.element(document.body).find('a[href]').last(); + lastAElement.focus(); + var $activeElement = angular.element(document.activeElement.tagName); + expect($activeElement).toHaveClass('modal'); + expect($document).toHaveModalsOpen(1); + + dismiss(modal); + expect($document).toHaveModalsOpen(0); + + aElement.remove(); + }); + it('should resolve returned promise on close', function () { var modal = open({template: '
Content
'}); close(modal, 'closed ok'); @@ -746,6 +769,51 @@ describe('$modal', function () { element.remove(); }); + + it('should support Tab and return focus to the current dialog', function () { + var link = 'Link'; + var currentModalID; + var aElement = angular.element(link); + angular.element(document.body).append(aElement); + aElement.focus(); + expect(document.activeElement.tagName).toBe('A'); + + //Open modal1 + var modal1 = open({template: '
Content
'}); + $timeout.flush(); + + //Focus to last link + var lastAElement = angular.element(document.body).find('a[href]').last(); + + //Focus outside of modal1 + expect($document).toHaveModalsOpen(1); + lastAElement.focus(); + currentModalID = document.activeElement.querySelector('div[id]').id; + expect(currentModalID).toBe('modal1'); + + //Open modal2 + var modal2 = open({template: '
Modal2
'}); + $timeout.flush(); + + //Focus outside of modal2 + expect($document).toHaveModalsOpen(2); + lastAElement.focus(); + currentModalID = document.activeElement.querySelector('div[id]').id; + expect(currentModalID).toBe('modal2'); + dismiss(modal2); + + //Focus change to modal1 + expect($document).toHaveModalsOpen(1); + currentModalID = document.activeElement.querySelector('div[id]').id; + expect(currentModalID).toBe('modal1'); + + + dismiss(modal1); + expect($document).toHaveModalsOpen(0); + + aElement.remove(); + }); + }); describe('modal.closing event', function() {