diff --git a/src/modal/docs/demo.js b/src/modal/docs/demo.js
index ad07089fbf..53b335dad1 100644
--- a/src/modal/docs/demo.js
+++ b/src/modal/docs/demo.js
@@ -1,10 +1,12 @@
-angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibModal, $log) {
+angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibModal, $log, $document) {
var $ctrl = this;
$ctrl.items = ['item1', 'item2', 'item3'];
$ctrl.animationsEnabled = true;
- $ctrl.open = function (size) {
+ $ctrl.open = function (size, parentSelector) {
+ var parentElem = parentSelector ?
+ angular.element($document[0].querySelector('.modal-demo ' + parentSelector)) : undefined;
var modalInstance = $uibModal.open({
animation: $ctrl.animationsEnabled,
ariaLabelledBy: 'modal-title',
@@ -13,6 +15,7 @@ angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibMo
controller: 'ModalInstanceCtrl',
controllerAs: '$ctrl',
size: size,
+ appendTo: parentElem,
resolve: {
items: function () {
return $ctrl.items;
@@ -45,6 +48,30 @@ angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibMo
});
};
+ $ctrl.openMultipleModals = function () {
+ $uibModal.open({
+ animation: $ctrl.animationsEnabled,
+ ariaLabelledBy: 'modal-title-bottom',
+ ariaDescribedBy: 'modal-body-bottom',
+ templateUrl: 'stackedModal.html',
+ size: 'sm',
+ controller: function($scope) {
+ $scope.name = 'bottom';
+ }
+ });
+
+ $uibModal.open({
+ animation: $ctrl.animationsEnabled,
+ ariaLabelledBy: 'modal-title-top',
+ ariaDescribedBy: 'modal-body-top',
+ templateUrl: 'stackedModal.html',
+ size: 'sm',
+ controller: function($scope) {
+ $scope.name = 'top';
+ }
+ });
+ };
+
$ctrl.toggleAnimation = function () {
$ctrl.animationsEnabled = !$ctrl.animationsEnabled;
};
diff --git a/src/modal/docs/readme.md b/src/modal/docs/readme.md
index 83db1aca12..66b6f36803 100644
--- a/src/modal/docs/readme.md
+++ b/src/modal/docs/readme.md
@@ -1,5 +1,5 @@
`$uibModal` is a service to create modal windows.
-Creating modals is straightforward: create a template, a controller and reference them when using `$uibModal`.
+Creating modals is straightforward: create a template and controller, and reference them when using `$uibModal`.
The `$uibModal` service has only one method: `open(options)`.
diff --git a/src/modal/modal.js b/src/modal/modal.js
index 1666152630..8c5a3df3ba 100644
--- a/src/modal/modal.js
+++ b/src/modal/modal.js
@@ -163,7 +163,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
// {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
scope.$isRendered = true;
- // Deferred object that will be resolved when this modal is render.
+ // Deferred object that will be resolved when this modal is rendered.
var modalRenderDeferObj = $q.defer();
// Resolve render promise post-digest
scope.$$postDigest(function() {
@@ -196,7 +196,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
/**
* If something within the freshly-opened modal already has focus (perhaps via a
- * directive that causes focus). then no need to try and focus anything.
+ * directive that causes focus) then there's no need to try to focus anything.
*/
if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) {
var inputWithAutofocus = element[0].querySelector('[autofocus]');
@@ -254,6 +254,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
};
var topModalIndex = 0;
var previousTopOpenedModal = null;
+ var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count';
//Modal focus behavior
var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' +
@@ -555,25 +556,74 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
openedWindows.top().value.modalDomEl = angularDomEl;
openedWindows.top().value.modalOpener = modalOpener;
+
+ applyAriaHidden(angularDomEl);
+
+ function applyAriaHidden(el) {
+ if (!el || el[0].tagName === 'BODY') {
+ return;
+ }
+
+ getSiblings(el).forEach(function(sibling) {
+ var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true',
+ ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10);
+
+ if (!ariaHiddenCount) {
+ ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0;
+ }
+
+ sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1);
+ sibling.setAttribute('aria-hidden', 'true');
+ });
+
+ return applyAriaHidden(el.parent());
+
+ function getSiblings(el) {
+ var children = el.parent() ? el.parent().children() : [];
+
+ return Array.prototype.filter.call(children, function(child) {
+ return child !== el[0];
+ });
+ }
+ }
};
function broadcastClosing(modalWindow, resultOrReason, closing) {
return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
}
+ function unhideBackgroundElements() {
+ Array.prototype.forEach.call(
+ document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'),
+ function(hiddenEl) {
+ var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10),
+ newHiddenCount = ariaHiddenCount - 1;
+ hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount);
+
+ if (!newHiddenCount) {
+ hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME);
+ hiddenEl.removeAttribute('aria-hidden');
+ }
+ }
+ );
+ }
+
$modalStack.close = function(modalInstance, result) {
var modalWindow = openedWindows.get(modalInstance);
+ unhideBackgroundElements();
if (modalWindow && broadcastClosing(modalWindow, result, true)) {
modalWindow.value.modalScope.$$uibDestructionScheduled = true;
modalWindow.value.deferred.resolve(result);
removeModalWindow(modalInstance, modalWindow.value.modalOpener);
return true;
}
+
return !modalWindow;
};
$modalStack.dismiss = function(modalInstance, reason) {
var modalWindow = openedWindows.get(modalInstance);
+ unhideBackgroundElements();
if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
modalWindow.value.modalScope.$$uibDestructionScheduled = true;
modalWindow.value.deferred.reject(reason);
@@ -596,6 +646,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
$modalStack.modalRendered = function(modalInstance) {
var modalWindow = openedWindows.get(modalInstance);
+ $modalStack.focusFirstFocusableElement($modalStack.loadFocusElementList(modalWindow));
if (modalWindow) {
modalWindow.value.renderDeferred.resolve();
}
diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js
index 98d3d258c9..5ab1939f1a 100644
--- a/src/modal/test/modal.spec.js
+++ b/src/modal/test/modal.spec.js
@@ -526,7 +526,7 @@ describe('$uibModal', function() {
var modal = open({template: '
Content
'});
$rootScope.$digest();
- expect(document.activeElement.tagName).toBe('DIV');
+ expect(document.activeElement.tagName).toBe('BUTTON');
expect($document).toHaveModalsOpen(1);
triggerKeyDown($document, 27);
@@ -656,7 +656,7 @@ describe('$uibModal', function() {
it('should not focus on the element that has autofocus attribute when the modal is opened and something in the modal already has focus and the animations have finished', function() {
function openAndCloseModalWithAutofocusElement() {
- var modal = open({template: ''});
+ var modal = open({template: ''});
$rootScope.$digest();
expect(angular.element('#auto-focus-element')).not.toHaveFocus();
expect(angular.element('#pre-focus-element')).toHaveFocus();
@@ -698,7 +698,7 @@ describe('$uibModal', function() {
$rootScope.$digest();
$animate.flush();
- expect(document.activeElement.tagName).toBe('DIV');
+ expect(document.activeElement.tagName).toBe('INPUT');
close(modal, 'closed ok');