Skip to content

Commit ad51c5b

Browse files
mikejr83David Dutch
authored andcommitted
feat(modal): 508 support in modal (mgcrea#2229)
* feat(modal): 508 support in modal 508 and screen-reader support in modal. * fix(eslint): Modal eslint errors Fix the eslint erros with modal.js. * fix(modal): fix skipped tests Remove the force flag from the modal tests. * fix(modal): aria-hidden on body without backdrop Only apply the aria-hidden to the body element when the backdrop option is specified.
1 parent 0d74fdf commit ad51c5b

5 files changed

Lines changed: 227 additions & 17 deletions

File tree

docs/dev.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
<body ontouchstart ng-controller="MainCtrl">
4747

4848
<div class="container">
49-
<!-- <div ng-include="'modal/docs/modal.demo.html'"></div> -->
49+
<div ng-include="'modal/docs/modal.demo.html'"></div>
5050
<!-- <div ng-include="'aside/docs/aside.demo.html'"></div> -->
5151
<!-- <div ng-include="'alert/docs/alert.demo.html'"></div> -->
5252
<!-- <div ng-include="'tooltip/docs/tooltip.demo.html'"></div> -->
@@ -58,7 +58,7 @@
5858
<!-- <div ng-include="'select/docs/select.demo.html'"></div> -->
5959
<!-- <div ng-include="'tab/docs/tab.demo.html'"></div> -->
6060
<!-- <div ng-include="'collapse/docs/collapse.demo.html'"></div> -->
61-
<div ng-include="'dropdown/docs/dropdown.demo.html'"></div>
61+
<!--<div ng-include="'dropdown/docs/dropdown.demo.html'"></div>-->
6262
<!-- <div ng-include="'navbar/docs/navbar.demo.html'"></div> -->
6363
<!-- <div ng-include="'scrollspy/docs/scrollspy.demo.html'"></div> -->
6464
<!-- <div ng-include="'affix/docs/affix.demo.html'"></div> -->

src/modal/docs/modal.demo.html

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ <h1 id="modals">Modals <a class="small" href="//github.com/mgcrea/angular-strap/
1010
<h2 id="modals-examples">Examples</h2>
1111
<p>Modals are streamlined, but flexible, dialog prompts with the minimum required functionality and smart defaults.</p>
1212

13-
<h3>Live demo <a class="small edit-plunkr" data-module-name="mgcrea.ngStrapDocs" data-content-html-url="modal/docs/modal.demo.html" data-content-js-url="modal/docs/modal.demo.js" ng-plunkr data-title="edit in plunker" data-placement="right" bs-tooltip>clog.info</a></h3>
13+
<h3>Live demo <a class="small edit-plunkr" data-module-name="mgcrea.ngStrapDocs" data-content-html-url="modal/docs/modal.demo.html"
14+
data-content-js-url="modal/docs/modal.demo.js" ng-plunkr data-title="edit in plunker" data-placement="right" bs-tooltip>clog.info</a></h3>
1415
<pre class="bs-example-scope">$scope.modal = {{modal | json}};</pre>
1516
<div class="bs-example" style="padding-bottom: 24px;" append-source>
1617

@@ -21,7 +22,8 @@ <h3>Live demo <a class="small edit-plunkr" data-module-name="mgcrea.ngStrapDocs"
2122
</button>
2223

2324
<!-- You can use a custom html template with the `data-template` attr -->
24-
<button type="button" class="btn btn-lg btn-danger" data-animation="am-fade-and-slide-top" data-template-url="modal/docs/modal.demo.tpl.html" bs-modal="modal">Custom Modal
25+
<button type="button" class="btn btn-lg btn-danger" data-animation="am-fade-and-slide-top" data-template-url="modal/docs/modal.demo.tpl.html"
26+
bs-modal="modal">Custom Modal
2527
<br />
2628
<small>(using data-template)</small>
2729
</button>
@@ -35,6 +37,20 @@ <h3>Live demo <a class="small edit-plunkr" data-module-name="mgcrea.ngStrapDocs"
3537

3638
</div>
3739

40+
<h3>508 Usage</h3>
41+
<p>
42+
For modal dialogs to be 508 and screen reader compliant please use the following examples. The <code>backdrop</code> must be set to <em>static</em> and
43+
<code>keyboard</code> must be set to a truthy value. Please be aware that the directive use-case handles returning focus
44+
to the item which opens the modal dialog. In scenarios where the <code>$modal</code> service is used it will be up to
45+
the implementation to return focus to the element which initiated the action to open the dialog.
46+
</p>
47+
<div class="bs-example" style="padding-bottom: 24px;" append-source>
48+
<button type="button" class="btn btn-primary" data-template-url="modal/docs/modal.demo.tpl.html" bs-modal="modal" keyboard="true"
49+
backdrop="static">
50+
Custom 508 Modal
51+
</button>
52+
</div>
53+
3854
<div class="callout callout-info">
3955
<h4>Backdrop animation</h4>
4056
<p>Backdrop animation being powered by <code>ngAnimate</code>, it requires custom CSS.</p>
@@ -87,8 +103,9 @@ <h4>The module also exposes a <code>$modal</code>service</h4>
87103

88104

89105
<h3>Options</h3>
90-
<p>Options can be passed via data-attributes on the directive or as an object hash to configure the service. For data attributes, append the option name to <code>data-</code>, as in <code>data-animation=""</code>.</p>
91-
<p>For directives, you can naturally inherit the contextual <code>$scope</code> or leverage a custom one with an <a href="http://docs.angularjs.org/guide/expression">AngularJS expression</a> to evaluate as an object directly on the <code>bs-modal</code> attribute</p>
106+
<p>Options can be passed via data-attributes on the directive or as an object hash to configure the service. For data attributes,
107+
append the option name to <code>data-</code>, as in <code>data-animation=""</code>.</p>
108+
<p>For directives, you can naturally inherit the contextual <code>$scope</code> or leverage a custom one with an <a href="http://docs.angularjs.org/guide/expression">AngularJS expression</a> to evaluate as an object directly on the <code>bs-modal</code> attribute</p>
92109
<div class="table-responsive">
93110
<table class="table table-bordered table-striped">
94111
<thead>
@@ -141,7 +158,8 @@ <h3>Options</h3>
141158
<td>boolean or the string <code>'static'</code>
142159
</td>
143160
<td>true</td>
144-
<td>Includes a modal-backdrop element. Alternatively, specify <code>static</code>for a backdrop which doesn't close the modal on click.</td>
161+
<td>Includes a modal-backdrop element. Alternatively, specify <code>static</code>for a backdrop which doesn't close
162+
the modal on click.</td>
145163
</tr>
146164
<tr>
147165
<td>keyboard</td>
@@ -160,15 +178,18 @@ <h3>Options</h3>
160178
<td>string | false</td>
161179
<td>false</td>
162180
<td>
163-
<p>Appends the popover to a specific element. Example: <code>container: 'body'</code>. This option is particularly useful in that it allows you to position the popover in the flow of the document near the triggering element -&nbsp;which will prevent the popover from floating away from the triggering element during a window resize.</p>
181+
<p>Appends the popover to a specific element. Example: <code>container: 'body'</code>. This option is particularly
182+
useful in that it allows you to position the popover in the flow of the document near the triggering element
183+
-&nbsp;which will prevent the popover from floating away from the triggering element during a window resize.</p>
164184
</td>
165185
</tr>
166186
<tr>
167187
<td>controller</td>
168188
<td>string|function</td>
169189
<td>false</td>
170190
<td>
171-
<p>Controller fn that should be associated with newly created scope or the name of a registered controller if passed as a string.</p>
191+
<p>Controller fn that should be associated with newly created scope or the name of a registered controller if passed
192+
as a string.</p>
172193
</td>
173194
</tr>
174195
<tr>
@@ -184,15 +205,17 @@ <h3>Options</h3>
184205
<td>object</td>
185206
<td>false</td>
186207
<td>
187-
<p>Object containing dependencies that will be injected into the controller's constructor when all the dependencies have resolved. The controller won't load if the promise is rejected.</p>
208+
<p>Object containing dependencies that will be injected into the controller's constructor when all the dependencies
209+
have resolved. The controller won't load if the promise is rejected.</p>
188210
</td>
189211
</tr>
190212
<tr>
191213
<td>locals</td>
192214
<td>object</td>
193215
<td>false</td>
194216
<td>
195-
<p>Object containing dependencies that will be injected into the controller's constructor. Similar to resolve but expects literal values instead of promises.</p>
217+
<p>Object containing dependencies that will be injected into the controller's constructor. Similar to resolve but
218+
expects literal values instead of promises.</p>
196219
</td>
197220
</tr>
198221
<tr>
@@ -209,23 +232,26 @@ <h3>Options</h3>
209232
<td>'modal/modal.tpl.html'</td>
210233
<td>
211234
<p>If provided, overrides the default template, can be either a remote URL or a cached template id.</p>
212-
<p>It should be a <code>div.modal</code> element following Bootstrap styles conventions (<a href="//github.com/mgcrea/angular-strap/blob/master/src/modal/modal.tpl.html" target="_blank">like this</a>).</p>
235+
<p>It should be a <code>div.modal</code> element following Bootstrap styles conventions (<a href="//github.com/mgcrea/angular-strap/blob/master/src/modal/modal.tpl.html"
236+
target="_blank">like this</a>).</p>
213237
</td>
214238
</tr>
215239
<tr>
216240
<td>contentTemplate</td>
217241
<td>path</td>
218242
<td>false</td>
219243
<td>
220-
<p>If provided, fetches the partial and includes it as the inner content, can be either a remote URL or a cached template id.</p>
244+
<p>If provided, fetches the partial and includes it as the inner content, can be either a remote URL or a cached
245+
template id.</p>
221246
</td>
222247
</tr>
223248
<tr>
224249
<td>prefixEvent</td>
225250
<td>string</td>
226251
<td>'modal'</td>
227252
<td>
228-
<p>If provided, prefixes the events '.hide.before' '.hide' '.show.before' and '.show' with the passed in value. With the default value these events are 'modal.hide.before' 'modal.hide' 'modal.show.before' and 'modal.show'</p>
253+
<p>If provided, prefixes the events '.hide.before' '.hide' '.show.before' and '.show' with the passed in value.
254+
With the default value these events are 'modal.hide.before' 'modal.hide' 'modal.show.before' and 'modal.show'</p>
229255
</td>
230256
</tr>
231257
<tr>
@@ -273,7 +299,7 @@ <h3>Options</h3>
273299
<td>number</td>
274300
<td>1050</td>
275301
<td>
276-
CSS z-index value for the modal.
302+
CSS z-index value for the modal.
277303
</td>
278304
</tr>
279305
</tbody>
@@ -308,4 +334,4 @@ <h4>$hide()</h4>
308334
<h4>$toggle()</h4>
309335
<p>Toggles the modal.</p>
310336

311-
</div>
337+
</div>

src/modal/modal.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
6262
// element id if defined
6363
$modal.$id = options.id || options.element && options.element.attr('id') || '';
6464

65+
$modal.returnFocus = function () {
66+
67+
};
68+
6569
// Support scope as string options
6670
forEach(['title', 'content'], function (key) {
6771
if (options[key]) scope[key] = $sce.trustAsHtml(options[key]);
@@ -209,6 +213,12 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
209213
});
210214

211215
bodyElement.addClass(options.prefixClass + '-open');
216+
// Add assistive attributes to the body to prevent the screen reader from reading it with the virtual keys
217+
// Only do this if the backdrop option is set.
218+
if (options.backdrop) {
219+
bodyElement.attr('aria-hidden', 'true');
220+
}
221+
212222
if (options.animation) {
213223
bodyElement.addClass(options.prefixClass + '-with-' + options.animation);
214224
}
@@ -224,6 +234,9 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
224234
if (angular.isDefined(options.onShow) && angular.isFunction(options.onShow)) {
225235
options.onShow($modal);
226236
}
237+
238+
modalElement.attr('aria-hidden', 'false');
239+
modalElement[0].focus();
227240
}
228241

229242
$modal.hide = function () {
@@ -236,6 +249,10 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
236249
options.onBeforeHide($modal);
237250
}
238251

252+
modalElement.attr('aria-hidden', 'true');
253+
254+
if ($modal.returnFocus && typeof $modal.returnFocus === 'function') $modal.returnFocus();
255+
239256
// Support v1.2+ $animate
240257
// https://github.com/angular/angular.js/issues/11713
241258
if (angular.version.minor <= 2) {
@@ -264,12 +281,42 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
264281
}
265282
if (findElement('.modal').length <= 0) {
266283
bodyElement.removeClass(options.prefixClass + '-open');
284+
if (options.backdrop) {
285+
bodyElement.attr('aria-hidden', 'false');
286+
}
267287
}
268288
if (options.animation) {
269289
bodyElement.removeClass(options.prefixClass + '-with-' + options.animation);
270290
}
271291
}
272292

293+
function findFocusableElements () {
294+
// Add all elements we want to include in our selection
295+
var focusableElements = 'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
296+
if (document.activeElement) {
297+
var focusable = Array.prototype.filter.call(modalElement[0].querySelectorAll(focusableElements),
298+
function (element) {
299+
// Check for visibility while always include the current activeElement
300+
return element.offsetWidth > 0 || element.offsetHeight > 0 || element === document.activeElement;
301+
});
302+
303+
return focusable;
304+
}
305+
}
306+
307+
function findNextFocusableElement (inReverse) {
308+
if (document.activeElement) {
309+
var focusable = findFocusableElements();
310+
if (focusable === undefined) return;
311+
if (inReverse) {
312+
focusable = Array.prototype.reverse.call(focusable);
313+
}
314+
315+
var index = focusable.indexOf(document.activeElement);
316+
return focusable[index + 1];
317+
}
318+
}
319+
273320
$modal.toggle = function () {
274321
if ($modal.$isShown) {
275322
$modal.hide();
@@ -286,11 +333,31 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
286333

287334
$modal.$onKeyUp = function (evt) {
288335

336+
// Escape was pressed on an open modal. Hide it.
289337
if (evt.which === 27 && $modal.$isShown) {
290338
$modal.hide();
291339
evt.stopPropagation();
292340
}
341+
};
293342

343+
$modal.$onKeyDown = function (evt) {
344+
if (options.keyboard) {
345+
if (evt.keyCode === 9) {
346+
347+
var nextFocusable = findNextFocusableElement(evt.shiftKey);
348+
if (nextFocusable === undefined) {
349+
if (evt.preventDefault) evt.preventDefault();
350+
if (evt.stopPropagation) evt.stopPropagation();
351+
352+
var focusable = findFocusableElements();
353+
if (evt.shiftKey) {
354+
focusable[focusable.length - 1].focus();
355+
} else {
356+
focusable[0].focus();
357+
}
358+
}
359+
}
360+
}
294361
};
295362

296363
function bindBackdropEvents () {
@@ -312,12 +379,14 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
312379
function bindKeyboardEvents () {
313380
if (options.keyboard) {
314381
modalElement.on('keyup', $modal.$onKeyUp);
382+
modalElement.on('keydown', $modal.$onKeyDown);
315383
}
316384
}
317385

318386
function unbindKeyboardEvents () {
319387
if (options.keyboard) {
320388
modalElement.off('keyup', $modal.$onKeyUp);
389+
modalElement.off('keydown', $modal.$onKeyDown);
321390
}
322391
}
323392

@@ -431,6 +500,12 @@ angular.module('mgcrea.ngStrap.modal', ['mgcrea.ngStrap.core', 'mgcrea.ngStrap.h
431500
// Initialize modal
432501
var modal = $modal(options);
433502

503+
if (options.keyboard) {
504+
modal.returnFocus = function () {
505+
element[0].focus();
506+
};
507+
}
508+
434509
// Trigger
435510
element.on(attr.trigger || 'click', modal.toggle);
436511

src/modal/modal.tpl.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div class="modal" tabindex="-1" role="dialog" aria-hidden="true">
1+
<div class="modal" tabindex="0" role="dialog" aria-hidden="true">
22
<!--<span role="dialog" class="sr-only"></span>-->
33
<div class="modal-dialog">
44
<div class="modal-content">

0 commit comments

Comments
 (0)