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

Commit b0c10c8

Browse files
fix(modal): Improve ARIA support by adding a live region.
chore(build): Fix package.json so grunt-cli does not need to be globally installed. chore(demo): Add command to run demo locally.
1 parent 1a132dd commit b0c10c8

File tree

7 files changed

+119
-11
lines changed

7 files changed

+119
-11
lines changed

Diff for: .travis.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ addons:
1313
before_install:
1414
- export DISPLAY=:99.0
1515
- sh -e /etc/init.d/xvfb start
16-
- npm install --quiet -g grunt-cli karma
16+
- npm install --quiet -g karma
1717

1818
script: grunt
1919
sudo: false

Diff for: package.json

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
],
1616
"main": "index.js",
1717
"scripts": {
18+
"demo": "grunt after-test && static dist -a 0.0.0.0 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}'",
1819
"test": "grunt"
1920
},
2021
"repository": {
@@ -26,6 +27,7 @@
2627
"angular-mocks": "1.5.8",
2728
"angular-sanitize": "1.5.8",
2829
"grunt": "^0.4.5",
30+
"grunt-cli": "^1.2.0",
2931
"grunt-contrib-concat": "^1.0.0",
3032
"grunt-contrib-copy": "^1.0.0",
3133
"grunt-contrib-uglify": "^1.0.1",
@@ -44,6 +46,7 @@
4446
"load-grunt-tasks": "^3.3.0",
4547
"lodash": "^4.1.0",
4648
"marked": "^0.3.5",
49+
"node-static": "^0.7.8",
4750
"semver": "^5.0.1",
4851
"shelljs": "^0.6.0"
4952
},

Diff for: src/modal/docs/demo.html

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div ng-controller="ModalDemoCtrl as $ctrl">
1+
<div ng-controller="ModalDemoCtrl as $ctrl" class="modal-demo">
22
<script type="text/ng-template" id="myModalContent.html">
33
<div class="modal-header">
44
<h3 class="modal-title" id="modal-title">I'm a modal!</h3>
@@ -16,11 +16,29 @@ <h3 class="modal-title" id="modal-title">I'm a modal!</h3>
1616
<button class="btn btn-warning" type="button" ng-click="$ctrl.cancel()">Cancel</button>
1717
</div>
1818
</script>
19+
<script type="text/ng-template" id="stackedModal.html">
20+
<div class="modal-header">
21+
<h3 class="modal-title" id="modal-title-{{name}}">The {{name}} modal!</h3>
22+
</div>
23+
<div class="modal-body" id="modal-body-{{name}}">
24+
Having multiple modals open at once is probably bad UX but it's technically possible.
25+
</div>
26+
</script>
1927

2028
<button type="button" class="btn btn-default" ng-click="$ctrl.open()">Open me!</button>
2129
<button type="button" class="btn btn-default" ng-click="$ctrl.open('lg')">Large modal</button>
2230
<button type="button" class="btn btn-default" ng-click="$ctrl.open('sm')">Small modal</button>
31+
<button type="button"
32+
class="btn btn-default"
33+
ng-click="$ctrl.open('sm', '.modal-parent')">
34+
Modal appended to a custom parent
35+
</button>
2336
<button type="button" class="btn btn-default" ng-click="$ctrl.toggleAnimation()">Toggle Animation ({{ $ctrl.animationsEnabled }})</button>
2437
<button type="button" class="btn btn-default" ng-click="$ctrl.openComponentModal()">Open a component modal!</button>
38+
<button type="button" class="btn btn-default" ng-click="$ctrl.openMultipleModals()">
39+
Open multiple modals at once
40+
</button>
2541
<div ng-show="$ctrl.selected">Selection from a modal: {{ $ctrl.selected }}</div>
42+
<div class="modal-parent">
43+
</div>
2644
</div>

Diff for: src/modal/docs/demo.js

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibModal, $log) {
1+
angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibModal, $log, $document) {
22
var $ctrl = this;
33
$ctrl.items = ['item1', 'item2', 'item3'];
44

55
$ctrl.animationsEnabled = true;
66

7-
$ctrl.open = function (size) {
7+
$ctrl.open = function (size, parentSelector) {
8+
var parentElem = parentSelector ?
9+
angular.element($document[0].querySelector('.modal-demo ' + parentSelector)) : undefined;
810
var modalInstance = $uibModal.open({
911
animation: $ctrl.animationsEnabled,
1012
ariaLabelledBy: 'modal-title',
@@ -13,6 +15,7 @@ angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibMo
1315
controller: 'ModalInstanceCtrl',
1416
controllerAs: '$ctrl',
1517
size: size,
18+
appendTo: parentElem,
1619
resolve: {
1720
items: function () {
1821
return $ctrl.items;
@@ -45,6 +48,30 @@ angular.module('ui.bootstrap.demo').controller('ModalDemoCtrl', function ($uibMo
4548
});
4649
};
4750

51+
$ctrl.openMultipleModals = function () {
52+
$uibModal.open({
53+
animation: $ctrl.animationsEnabled,
54+
ariaLabelledBy: 'modal-title-bottom',
55+
ariaDescribedBy: 'modal-body-bottom',
56+
templateUrl: 'stackedModal.html',
57+
size: 'sm',
58+
controller: function($scope) {
59+
$scope.name = 'bottom';
60+
}
61+
});
62+
63+
$uibModal.open({
64+
animation: $ctrl.animationsEnabled,
65+
ariaLabelledBy: 'modal-title-top',
66+
ariaDescribedBy: 'modal-body-top',
67+
templateUrl: 'stackedModal.html',
68+
size: 'sm',
69+
controller: function($scope) {
70+
$scope.name = 'top';
71+
}
72+
});
73+
};
74+
4875
$ctrl.toggleAnimation = function () {
4976
$ctrl.animationsEnabled = !$ctrl.animationsEnabled;
5077
};

Diff for: src/modal/docs/readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
`$uibModal` is a service to create modal windows.
2-
Creating modals is straightforward: create a template, a controller and reference them when using `$uibModal`.
2+
Creating modals is straightforward: create a template and controller, and reference them when using `$uibModal`.
33

44
The `$uibModal` service has only one method: `open(options)`.
55

Diff for: src/modal/modal.js

+53-3
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
163163
// {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}.
164164
scope.$isRendered = true;
165165

166-
// Deferred object that will be resolved when this modal is render.
166+
// Deferred object that will be resolved when this modal is rendered.
167167
var modalRenderDeferObj = $q.defer();
168168
// Resolve render promise post-digest
169169
scope.$$postDigest(function() {
@@ -196,7 +196,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
196196

197197
/**
198198
* If something within the freshly-opened modal already has focus (perhaps via a
199-
* directive that causes focus). then no need to try and focus anything.
199+
* directive that causes focus) then there's no need to try to focus anything.
200200
*/
201201
if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) {
202202
var inputWithAutofocus = element[0].querySelector('[autofocus]');
@@ -254,6 +254,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
254254
};
255255
var topModalIndex = 0;
256256
var previousTopOpenedModal = null;
257+
var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count';
257258

258259
//Modal focus behavior
259260
var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' +
@@ -529,6 +530,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
529530
'role': 'dialog',
530531
'aria-labelledby': modal.ariaLabelledBy,
531532
'aria-describedby': modal.ariaDescribedBy,
533+
'aria-live': 'polite',
532534
'size': modal.size,
533535
'index': topModalIndex,
534536
'animate': 'animate',
@@ -555,20 +557,67 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
555557

556558
openedWindows.top().value.modalDomEl = angularDomEl;
557559
openedWindows.top().value.modalOpener = modalOpener;
560+
561+
applyAriaHidden(angularDomEl);
562+
563+
function applyAriaHidden(el) {
564+
if (!el || el[0].tagName === 'BODY') {
565+
return;
566+
}
567+
568+
getSiblings(el).forEach(function(sibling) {
569+
var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true',
570+
ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10);
571+
572+
if (!ariaHiddenCount) {
573+
ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0;
574+
}
575+
576+
sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1);
577+
sibling.setAttribute('aria-hidden', 'true');
578+
});
579+
580+
return applyAriaHidden(el.parent());
581+
582+
function getSiblings(el) {
583+
var children = el.parent() ? el.parent().children() : [];
584+
585+
return Array.prototype.filter.call(children, function(child) {
586+
return child !== el;
587+
});
588+
}
589+
}
558590
};
559591

560592
function broadcastClosing(modalWindow, resultOrReason, closing) {
561593
return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented;
562594
}
563595

564596
$modalStack.close = function(modalInstance, result) {
565-
var modalWindow = openedWindows.get(modalInstance);
597+
var modalWindow;
598+
599+
Array.prototype.forEach.call(
600+
document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'),
601+
function(hiddenEl) {
602+
var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10),
603+
newHiddenCount = ariaHiddenCount - 1;
604+
hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount);
605+
606+
if (!newHiddenCount) {
607+
hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME);
608+
hiddenEl.removeAttribute('aria-hidden');
609+
}
610+
}
611+
);
612+
613+
modalWindow = openedWindows.get(modalInstance);
566614
if (modalWindow && broadcastClosing(modalWindow, result, true)) {
567615
modalWindow.value.modalScope.$$uibDestructionScheduled = true;
568616
modalWindow.value.deferred.resolve(result);
569617
removeModalWindow(modalInstance, modalWindow.value.modalOpener);
570618
return true;
571619
}
620+
572621
return !modalWindow;
573622
};
574623

@@ -596,6 +645,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p
596645

597646
$modalStack.modalRendered = function(modalInstance) {
598647
var modalWindow = openedWindows.get(modalInstance);
648+
$modalStack.focusFirstFocusableElement($modalStack.loadFocusElementList(modalWindow));
599649
if (modalWindow) {
600650
modalWindow.value.renderDeferred.resolve();
601651
}

Diff for: src/modal/test/modal.spec.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ describe('$uibModal', function() {
526526

527527
var modal = open({template: '<div>Content<button>inside modal</button></div>'});
528528
$rootScope.$digest();
529-
expect(document.activeElement.tagName).toBe('DIV');
529+
expect(document.activeElement.tagName).toBe('BUTTON');
530530
expect($document).toHaveModalsOpen(1);
531531

532532
triggerKeyDown($document, 27);
@@ -656,7 +656,7 @@ describe('$uibModal', function() {
656656
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() {
657657
function openAndCloseModalWithAutofocusElement() {
658658

659-
var modal = open({template: '<div><input type="text" id="auto-focus-element" autofocus><input type="text" id="pre-focus-element" focus-me></div>'});
659+
var modal = open({template: '<div><input type="text" id="pre-focus-element" focus-me><input type="text" id="auto-focus-element" autofocus></div>'});
660660
$rootScope.$digest();
661661
expect(angular.element('#auto-focus-element')).not.toHaveFocus();
662662
expect(angular.element('#pre-focus-element')).toHaveFocus();
@@ -698,7 +698,7 @@ describe('$uibModal', function() {
698698
$rootScope.$digest();
699699
$animate.flush();
700700

701-
expect(document.activeElement.tagName).toBe('DIV');
701+
expect(document.activeElement.tagName).toBe('INPUT');
702702

703703
close(modal, 'closed ok');
704704

@@ -1586,6 +1586,16 @@ describe('$uibModal', function() {
15861586
expect($document.find('.modal').attr('aria-describedby')).toEqual('modal-description');
15871587
});
15881588
});
1589+
1590+
describe('ariaLive', function() {
1591+
it('should add the aria-live property to the modal', function() {
1592+
open({
1593+
template: '<p>Modal content</p>'
1594+
});
1595+
1596+
expect($document.find('.modal').attr('aria-live')).toEqual('polite');
1597+
});
1598+
});
15891599
});
15901600

15911601
describe('modal window', function() {

0 commit comments

Comments
 (0)