Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

Commit

Permalink
fix(modal): Captures focus in both directions - using tab & shift-tab
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobino committed May 20, 2015
1 parent 2a2e5de commit 5ae356f
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 1 deletion.
61 changes: 60 additions & 1 deletion src/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<div tabindex="0"></div>');
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 {
Expand Down Expand Up @@ -291,7 +350,7 @@ angular.module('ui.bootstrap.modal', [])
body.append(backdropDomEl);
}

var angularDomEl = angular.element('<div modal-window="modal-window"></div>');
var angularDomEl = angular.element('<div enforce-focus modal-window="modal-window"></div>');
angularDomEl.attr({
'template-url': modal.windowTemplateUrl,
'window-class': modal.windowClass,
Expand Down
68 changes: 68 additions & 0 deletions src/modal/test/modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,29 @@ describe('$modal', function () {
element.remove();
});

it('should support Tab and return focus to the dialog', function () {
var link = '<a href>Link</a>';
var aElement = angular.element(link);
angular.element(document.body).append(aElement);
aElement.focus();
expect(document.activeElement.tagName).toBe('A');

var modal = open({template: '<div>Content<input type="text"><button>inside modal</button></div>'});
$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: '<div>Content</div>'});
close(modal, 'closed ok');
Expand Down Expand Up @@ -746,6 +769,51 @@ describe('$modal', function () {

element.remove();
});

it('should support Tab and return focus to the current dialog', function () {
var link = '<a href>Link</a>';
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: '<div id="modal1">Content<input type="text"><button>inside modal1</button></div>'});
$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: '<div id="modal2">Modal2</div>'});
$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() {
Expand Down

0 comments on commit 5ae356f

Please sign in to comment.