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

fix(modal): Wait for animation before focus. #4300

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 21 additions & 16 deletions src/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,15 @@ angular.module('ui.bootstrap.modal', [])
});

modalRenderDeferObj.promise.then(function() {
var animationPromise = null;

if (attrs.modalInClass) {
if ($animateCss) {
$animateCss(element, {
animationPromise = $animateCss(element, {
addClass: attrs.modalInClass
}).start();
} else {
$animate.addClass(element, attrs.modalInClass);
animationPromise = $animate.addClass(element, attrs.modalInClass);
}

scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) {
Expand All @@ -224,20 +226,23 @@ angular.module('ui.bootstrap.modal', [])
});
}

var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
/**
* Auto-focusing of a freshly-opened modal element causes any child elements
* with the autofocus attribute to lose focus. This is an issue on touch
* based devices which will show and then hide the onscreen keyboard.
* Attempts to refocus the autofocus element via JavaScript will not reopen
* the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
* the modal element if the modal does not contain an autofocus element.
*/
if (inputsWithAutofocus.length) {
inputsWithAutofocus[0].focus();
} else {
element[0].focus();
}

$q.when(animationPromise).then(function() {
var inputsWithAutofocus = element[0].querySelectorAll('[autofocus]');
/**
* Auto-focusing of a freshly-opened modal element causes any child elements
* with the autofocus attribute to lose focus. This is an issue on touch
* based devices which will show and then hide the onscreen keyboard.
* Attempts to refocus the autofocus element via JavaScript will not reopen
* the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
* the modal element if the modal does not contain an autofocus element.
*/
if (inputsWithAutofocus.length) {
inputsWithAutofocus[0].focus();
} else {
element[0].focus();
}
});

// Notify {@link $modalStack} that modal is rendered.
var modal = $modalStack.getTop();
Expand Down
45 changes: 43 additions & 2 deletions src/modal/test/modal.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ describe('$modal', function () {
expect(document.activeElement.tagName).toBe('A');

var modal = open({template: '<div>Content<button>inside modal</button></div>'});
$animate.triggerCallbacks();
expect(document.activeElement.tagName).toBe('DIV');
expect($document).toHaveModalsOpen(1);

Expand All @@ -307,6 +308,7 @@ describe('$modal', function () {
expect(document.activeElement.tagName).toBe('A');

var modal = open({template: '<div>Content</div>'});
$animate.triggerCallbacks();
expect(document.activeElement.tagName).toBe('DIV');
expect($document).toHaveModalsOpen(1);

Expand Down Expand Up @@ -366,10 +368,10 @@ describe('$modal', function () {
expect(modal.opened).toBeRejectedWith('ko');
});

it('should focus on the element that has autofocus attribute when the modal is open/reopen', function() {
it('should focus on the element that has autofocus attribute when the modal is open/reopen and the animations have finished', function() {
function openAndCloseModalWithAutofocusElement() {
var modal = open({template: '<div><input type="text" id="auto-focus-element" autofocus></div>'});

$animate.triggerCallbacks();
expect(angular.element('#auto-focus-element')).toHaveFocus();

close(modal, 'closed ok');
Expand All @@ -381,6 +383,43 @@ describe('$modal', function () {
openAndCloseModalWithAutofocusElement();
});

it('should wait until the in animation is finished before attempting to focus the modal or autofocus element', function() {
function openAndCloseModalWithAutofocusElement() {
var modal = open({template: '<div><input type="text" id="auto-focus-element" autofocus></div>'});
expect(angular.element('#auto-focus-element')).not.toHaveFocus();

$animate.triggerCallbacks();

expect(angular.element('#auto-focus-element')).toHaveFocus();

close(modal, 'closed ok');

expect(modal.result).toBeResolvedWith('closed ok');
}

function openAndCloseModalWithOutAutofocusElement() {
var link = '<a href>Link</a>';
var element = angular.element(link);
angular.element(document.body).append(element);
element.focus();
expect(document.activeElement.tagName).toBe('A');

var modal = open({template: '<div><input type="text"></div>'});
expect(document.activeElement.tagName).toBe('A');

$animate.triggerCallbacks();

expect(document.activeElement.tagName).toBe('DIV');

close(modal, 'closed ok');

expect(modal.result).toBeResolvedWith('closed ok');
}

openAndCloseModalWithAutofocusElement();
openAndCloseModalWithOutAutofocusElement();
});

it('should change focus to first element when tab key was pressed', function() {
var initialPage = angular.element('<a href="#" id="cannot-get-focus-from-modal">Outland link</a>');
angular.element(document.body).append(initialPage);
Expand Down Expand Up @@ -907,11 +946,13 @@ describe('$modal', function () {
expect(document.activeElement.tagName).toBe('A');

var modal1 = open({template: '<div>Modal1<button id="focus">inside modal1</button></div>'});
$animate.triggerCallbacks();
document.getElementById('focus').focus();
expect(document.activeElement.tagName).toBe('BUTTON');
expect($document).toHaveModalsOpen(1);

var modal2 = open({template: '<div>Modal2</div>'});
$animate.triggerCallbacks();
expect(document.activeElement.tagName).toBe('DIV');
expect($document).toHaveModalsOpen(2);

Expand Down