From ebedb44ecf7040a08c77d944bb746822465431fc Mon Sep 17 00:00:00 2001 From: Paulo Pacheco Date: Thu, 23 Jul 2015 08:50:46 -0300 Subject: [PATCH 1/2] Fix Modal: Focus is now trapped when modal is opened. It works in both directions with tab or shift+tab. Refs: #738, #3689 --- src/modal/docs/demo.html | 4 +- src/modal/modal.js | 91 ++++++++++++++++++++++++++++++++---- src/modal/test/modal.spec.js | 38 ++++++++++++++- 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/src/modal/docs/demo.html b/src/modal/docs/demo.html index 48302c7065..d48b832573 100644 --- a/src/modal/docs/demo.html +++ b/src/modal/docs/demo.html @@ -6,7 +6,7 @@ \ No newline at end of file + 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 () { From 83671d67a316c9562c3d9e10abeddca0daa9a1d1 Mon Sep 17 00:00:00 2001 From: Paulo Pacheco Date: Thu, 23 Jul 2015 09:11:12 -0300 Subject: [PATCH 2/2] The "new line" key was removed. --- src/modal/docs/demo.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modal/docs/demo.html b/src/modal/docs/demo.html index d48b832573..4109628073 100644 --- a/src/modal/docs/demo.html +++ b/src/modal/docs/demo.html @@ -22,4 +22,4 @@
Selection from a modal: {{ selected }}
- + \ No newline at end of file