1 | /**
|
---|
2 | * @license AngularJS v1.8.2
|
---|
3 | * (c) 2010-2020 Google LLC. http://angularjs.org
|
---|
4 | * License: MIT
|
---|
5 | */
|
---|
6 | (function(window, angular) {'use strict';
|
---|
7 |
|
---|
8 | /**
|
---|
9 | * @ngdoc module
|
---|
10 | * @name ngAria
|
---|
11 | * @description
|
---|
12 | *
|
---|
13 | * The `ngAria` module provides support for common
|
---|
14 | * [<abbr title="Accessible Rich Internet Applications">ARIA</abbr>](http://www.w3.org/TR/wai-aria/)
|
---|
15 | * attributes that convey state or semantic information about the application for users
|
---|
16 | * of assistive technologies, such as screen readers.
|
---|
17 | *
|
---|
18 | * ## Usage
|
---|
19 | *
|
---|
20 | * For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
|
---|
21 | * directives are supported:
|
---|
22 | * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`,
|
---|
23 | * `ngClick`, `ngDblClick`, and `ngMessages`.
|
---|
24 | *
|
---|
25 | * Below is a more detailed breakdown of the attributes handled by ngAria:
|
---|
26 | *
|
---|
27 | * | Directive | Supported Attributes |
|
---|
28 | * |---------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
---|
29 | * | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles |
|
---|
30 | * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
|
---|
31 | * | {@link ng.directive:ngRequired ngRequired} | aria-required |
|
---|
32 | * | {@link ng.directive:ngChecked ngChecked} | aria-checked |
|
---|
33 | * | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly |
|
---|
34 | * | {@link ng.directive:ngValue ngValue} | aria-checked |
|
---|
35 | * | {@link ng.directive:ngShow ngShow} | aria-hidden |
|
---|
36 | * | {@link ng.directive:ngHide ngHide} | aria-hidden |
|
---|
37 | * | {@link ng.directive:ngDblclick ngDblclick} | tabindex |
|
---|
38 | * | {@link module:ngMessages ngMessages} | aria-live |
|
---|
39 | * | {@link ng.directive:ngClick ngClick} | tabindex, keydown event, button role |
|
---|
40 | *
|
---|
41 | * Find out more information about each directive by reading the
|
---|
42 | * {@link guide/accessibility ngAria Developer Guide}.
|
---|
43 | *
|
---|
44 | * ## Example
|
---|
45 | * Using ngDisabled with ngAria:
|
---|
46 | * ```html
|
---|
47 | * <md-checkbox ng-disabled="disabled">
|
---|
48 | * ```
|
---|
49 | * Becomes:
|
---|
50 | * ```html
|
---|
51 | * <md-checkbox ng-disabled="disabled" aria-disabled="true">
|
---|
52 | * ```
|
---|
53 | *
|
---|
54 | * ## Disabling Specific Attributes
|
---|
55 | * It is possible to disable individual attributes added by ngAria with the
|
---|
56 | * {@link ngAria.$ariaProvider#config config} method. For more details, see the
|
---|
57 | * {@link guide/accessibility Developer Guide}.
|
---|
58 | *
|
---|
59 | * ## Disabling `ngAria` on Specific Elements
|
---|
60 | * It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable`
|
---|
61 | * attribute on it. Note that only the element itself (and not its child elements) will be ignored.
|
---|
62 | */
|
---|
63 | var ARIA_DISABLE_ATTR = 'ngAriaDisable';
|
---|
64 |
|
---|
65 | var ngAriaModule = angular.module('ngAria', ['ng']).
|
---|
66 | info({ angularVersion: '1.8.2' }).
|
---|
67 | provider('$aria', $AriaProvider);
|
---|
68 |
|
---|
69 | /**
|
---|
70 | * Internal Utilities
|
---|
71 | */
|
---|
72 | var nativeAriaNodeNames = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY'];
|
---|
73 |
|
---|
74 | var isNodeOneOf = function(elem, nodeTypeArray) {
|
---|
75 | if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
|
---|
76 | return true;
|
---|
77 | }
|
---|
78 | };
|
---|
79 | /**
|
---|
80 | * @ngdoc provider
|
---|
81 | * @name $ariaProvider
|
---|
82 | * @this
|
---|
83 | *
|
---|
84 | * @description
|
---|
85 | *
|
---|
86 | * Used for configuring the ARIA attributes injected and managed by ngAria.
|
---|
87 | *
|
---|
88 | * ```js
|
---|
89 | * angular.module('myApp', ['ngAria'], function config($ariaProvider) {
|
---|
90 | * $ariaProvider.config({
|
---|
91 | * ariaValue: true,
|
---|
92 | * tabindex: false
|
---|
93 | * });
|
---|
94 | * });
|
---|
95 | *```
|
---|
96 | *
|
---|
97 | * ## Dependencies
|
---|
98 | * Requires the {@link ngAria} module to be installed.
|
---|
99 | *
|
---|
100 | */
|
---|
101 | function $AriaProvider() {
|
---|
102 | var config = {
|
---|
103 | ariaHidden: true,
|
---|
104 | ariaChecked: true,
|
---|
105 | ariaReadonly: true,
|
---|
106 | ariaDisabled: true,
|
---|
107 | ariaRequired: true,
|
---|
108 | ariaInvalid: true,
|
---|
109 | ariaValue: true,
|
---|
110 | tabindex: true,
|
---|
111 | bindKeydown: true,
|
---|
112 | bindRoleForClick: true
|
---|
113 | };
|
---|
114 |
|
---|
115 | /**
|
---|
116 | * @ngdoc method
|
---|
117 | * @name $ariaProvider#config
|
---|
118 | *
|
---|
119 | * @param {object} config object to enable/disable specific ARIA attributes
|
---|
120 | *
|
---|
121 | * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags
|
---|
122 | * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags
|
---|
123 | * - **ariaReadonly** – `{boolean}` – Enables/disables aria-readonly tags
|
---|
124 | * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags
|
---|
125 | * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags
|
---|
126 | * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags
|
---|
127 | * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and
|
---|
128 | * aria-valuenow tags
|
---|
129 | * - **tabindex** – `{boolean}` – Enables/disables tabindex tags
|
---|
130 | * - **bindKeydown** – `{boolean}` – Enables/disables keyboard event binding on non-interactive
|
---|
131 | * elements (such as `div` or `li`) using ng-click, making them more accessible to users of
|
---|
132 | * assistive technologies
|
---|
133 | * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements (such as
|
---|
134 | * `div` or `li`) using ng-click, making them more accessible to users of assistive
|
---|
135 | * technologies
|
---|
136 | *
|
---|
137 | * @description
|
---|
138 | * Enables/disables various ARIA attributes
|
---|
139 | */
|
---|
140 | this.config = function(newConfig) {
|
---|
141 | config = angular.extend(config, newConfig);
|
---|
142 | };
|
---|
143 |
|
---|
144 | function watchExpr(attrName, ariaAttr, nativeAriaNodeNames, negate) {
|
---|
145 | return function(scope, elem, attr) {
|
---|
146 | if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
---|
147 |
|
---|
148 | var ariaCamelName = attr.$normalize(ariaAttr);
|
---|
149 | if (config[ariaCamelName] && !isNodeOneOf(elem, nativeAriaNodeNames) && !attr[ariaCamelName]) {
|
---|
150 | scope.$watch(attr[attrName], function(boolVal) {
|
---|
151 | // ensure boolean value
|
---|
152 | boolVal = negate ? !boolVal : !!boolVal;
|
---|
153 | elem.attr(ariaAttr, boolVal);
|
---|
154 | });
|
---|
155 | }
|
---|
156 | };
|
---|
157 | }
|
---|
158 | /**
|
---|
159 | * @ngdoc service
|
---|
160 | * @name $aria
|
---|
161 | *
|
---|
162 | * @description
|
---|
163 | *
|
---|
164 | * The $aria service contains helper methods for applying common
|
---|
165 | * [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives.
|
---|
166 | *
|
---|
167 | * ngAria injects common accessibility attributes that tell assistive technologies when HTML
|
---|
168 | * elements are enabled, selected, hidden, and more. To see how this is performed with ngAria,
|
---|
169 | * let's review a code snippet from ngAria itself:
|
---|
170 | *
|
---|
171 | *```js
|
---|
172 | * ngAriaModule.directive('ngDisabled', ['$aria', function($aria) {
|
---|
173 | * return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nativeAriaNodeNames, false);
|
---|
174 | * }])
|
---|
175 | *```
|
---|
176 | * Shown above, the ngAria module creates a directive with the same signature as the
|
---|
177 | * traditional `ng-disabled` directive. But this ngAria version is dedicated to
|
---|
178 | * solely managing accessibility attributes on custom elements. The internal `$aria` service is
|
---|
179 | * used to watch the boolean attribute `ngDisabled`. If it has not been explicitly set by the
|
---|
180 | * developer, `aria-disabled` is injected as an attribute with its value synchronized to the
|
---|
181 | * value in `ngDisabled`.
|
---|
182 | *
|
---|
183 | * Because ngAria hooks into the `ng-disabled` directive, developers do not have to do
|
---|
184 | * anything to enable this feature. The `aria-disabled` attribute is automatically managed
|
---|
185 | * simply as a silent side-effect of using `ng-disabled` with the ngAria module.
|
---|
186 | *
|
---|
187 | * The full list of directives that interface with ngAria:
|
---|
188 | * * **ngModel**
|
---|
189 | * * **ngChecked**
|
---|
190 | * * **ngReadonly**
|
---|
191 | * * **ngRequired**
|
---|
192 | * * **ngDisabled**
|
---|
193 | * * **ngValue**
|
---|
194 | * * **ngShow**
|
---|
195 | * * **ngHide**
|
---|
196 | * * **ngClick**
|
---|
197 | * * **ngDblclick**
|
---|
198 | * * **ngMessages**
|
---|
199 | *
|
---|
200 | * Read the {@link guide/accessibility ngAria Developer Guide} for a thorough explanation of each
|
---|
201 | * directive.
|
---|
202 | *
|
---|
203 | *
|
---|
204 | * ## Dependencies
|
---|
205 | * Requires the {@link ngAria} module to be installed.
|
---|
206 | */
|
---|
207 | this.$get = function() {
|
---|
208 | return {
|
---|
209 | config: function(key) {
|
---|
210 | return config[key];
|
---|
211 | },
|
---|
212 | $$watchExpr: watchExpr
|
---|
213 | };
|
---|
214 | };
|
---|
215 | }
|
---|
216 |
|
---|
217 |
|
---|
218 | ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
---|
219 | return $aria.$$watchExpr('ngShow', 'aria-hidden', [], true);
|
---|
220 | }])
|
---|
221 | .directive('ngHide', ['$aria', function($aria) {
|
---|
222 | return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false);
|
---|
223 | }])
|
---|
224 | .directive('ngValue', ['$aria', function($aria) {
|
---|
225 | return $aria.$$watchExpr('ngValue', 'aria-checked', nativeAriaNodeNames, false);
|
---|
226 | }])
|
---|
227 | .directive('ngChecked', ['$aria', function($aria) {
|
---|
228 | return $aria.$$watchExpr('ngChecked', 'aria-checked', nativeAriaNodeNames, false);
|
---|
229 | }])
|
---|
230 | .directive('ngReadonly', ['$aria', function($aria) {
|
---|
231 | return $aria.$$watchExpr('ngReadonly', 'aria-readonly', nativeAriaNodeNames, false);
|
---|
232 | }])
|
---|
233 | .directive('ngRequired', ['$aria', function($aria) {
|
---|
234 | return $aria.$$watchExpr('ngRequired', 'aria-required', nativeAriaNodeNames, false);
|
---|
235 | }])
|
---|
236 | .directive('ngModel', ['$aria', function($aria) {
|
---|
237 |
|
---|
238 | function shouldAttachAttr(attr, normalizedAttr, elem, allowNonAriaNodes) {
|
---|
239 | return $aria.config(normalizedAttr) &&
|
---|
240 | !elem.attr(attr) &&
|
---|
241 | (allowNonAriaNodes || !isNodeOneOf(elem, nativeAriaNodeNames)) &&
|
---|
242 | (elem.attr('type') !== 'hidden' || elem[0].nodeName !== 'INPUT');
|
---|
243 | }
|
---|
244 |
|
---|
245 | function shouldAttachRole(role, elem) {
|
---|
246 | // if element does not have role attribute
|
---|
247 | // AND element type is equal to role (if custom element has a type equaling shape) <-- remove?
|
---|
248 | // AND element is not in nativeAriaNodeNames
|
---|
249 | return !elem.attr('role') && (elem.attr('type') === role) && !isNodeOneOf(elem, nativeAriaNodeNames);
|
---|
250 | }
|
---|
251 |
|
---|
252 | function getShape(attr, elem) {
|
---|
253 | var type = attr.type,
|
---|
254 | role = attr.role;
|
---|
255 |
|
---|
256 | return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' :
|
---|
257 | ((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' :
|
---|
258 | (type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' : '';
|
---|
259 | }
|
---|
260 |
|
---|
261 | return {
|
---|
262 | restrict: 'A',
|
---|
263 | require: 'ngModel',
|
---|
264 | priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
|
---|
265 | compile: function(elem, attr) {
|
---|
266 | if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
---|
267 |
|
---|
268 | var shape = getShape(attr, elem);
|
---|
269 |
|
---|
270 | return {
|
---|
271 | post: function(scope, elem, attr, ngModel) {
|
---|
272 | var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem, false);
|
---|
273 |
|
---|
274 | function ngAriaWatchModelValue() {
|
---|
275 | return ngModel.$modelValue;
|
---|
276 | }
|
---|
277 |
|
---|
278 | function getRadioReaction(newVal) {
|
---|
279 | // Strict comparison would cause a BC
|
---|
280 | // eslint-disable-next-line eqeqeq
|
---|
281 | var boolVal = (attr.value == ngModel.$viewValue);
|
---|
282 | elem.attr('aria-checked', boolVal);
|
---|
283 | }
|
---|
284 |
|
---|
285 | function getCheckboxReaction() {
|
---|
286 | elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue));
|
---|
287 | }
|
---|
288 |
|
---|
289 | switch (shape) {
|
---|
290 | case 'radio':
|
---|
291 | case 'checkbox':
|
---|
292 | if (shouldAttachRole(shape, elem)) {
|
---|
293 | elem.attr('role', shape);
|
---|
294 | }
|
---|
295 | if (shouldAttachAttr('aria-checked', 'ariaChecked', elem, false)) {
|
---|
296 | scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
|
---|
297 | getRadioReaction : getCheckboxReaction);
|
---|
298 | }
|
---|
299 | if (needsTabIndex) {
|
---|
300 | elem.attr('tabindex', 0);
|
---|
301 | }
|
---|
302 | break;
|
---|
303 | case 'range':
|
---|
304 | if (shouldAttachRole(shape, elem)) {
|
---|
305 | elem.attr('role', 'slider');
|
---|
306 | }
|
---|
307 | if ($aria.config('ariaValue')) {
|
---|
308 | var needsAriaValuemin = !elem.attr('aria-valuemin') &&
|
---|
309 | (attr.hasOwnProperty('min') || attr.hasOwnProperty('ngMin'));
|
---|
310 | var needsAriaValuemax = !elem.attr('aria-valuemax') &&
|
---|
311 | (attr.hasOwnProperty('max') || attr.hasOwnProperty('ngMax'));
|
---|
312 | var needsAriaValuenow = !elem.attr('aria-valuenow');
|
---|
313 |
|
---|
314 | if (needsAriaValuemin) {
|
---|
315 | attr.$observe('min', function ngAriaValueMinReaction(newVal) {
|
---|
316 | elem.attr('aria-valuemin', newVal);
|
---|
317 | });
|
---|
318 | }
|
---|
319 | if (needsAriaValuemax) {
|
---|
320 | attr.$observe('max', function ngAriaValueMinReaction(newVal) {
|
---|
321 | elem.attr('aria-valuemax', newVal);
|
---|
322 | });
|
---|
323 | }
|
---|
324 | if (needsAriaValuenow) {
|
---|
325 | scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) {
|
---|
326 | elem.attr('aria-valuenow', newVal);
|
---|
327 | });
|
---|
328 | }
|
---|
329 | }
|
---|
330 | if (needsTabIndex) {
|
---|
331 | elem.attr('tabindex', 0);
|
---|
332 | }
|
---|
333 | break;
|
---|
334 | }
|
---|
335 |
|
---|
336 | if (!attr.hasOwnProperty('ngRequired') && ngModel.$validators.required
|
---|
337 | && shouldAttachAttr('aria-required', 'ariaRequired', elem, false)) {
|
---|
338 | // ngModel.$error.required is undefined on custom controls
|
---|
339 | attr.$observe('required', function() {
|
---|
340 | elem.attr('aria-required', !!attr['required']);
|
---|
341 | });
|
---|
342 | }
|
---|
343 |
|
---|
344 | if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem, true)) {
|
---|
345 | scope.$watch(function ngAriaInvalidWatch() {
|
---|
346 | return ngModel.$invalid;
|
---|
347 | }, function ngAriaInvalidReaction(newVal) {
|
---|
348 | elem.attr('aria-invalid', !!newVal);
|
---|
349 | });
|
---|
350 | }
|
---|
351 | }
|
---|
352 | };
|
---|
353 | }
|
---|
354 | };
|
---|
355 | }])
|
---|
356 | .directive('ngDisabled', ['$aria', function($aria) {
|
---|
357 | return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nativeAriaNodeNames, false);
|
---|
358 | }])
|
---|
359 | .directive('ngMessages', function() {
|
---|
360 | return {
|
---|
361 | restrict: 'A',
|
---|
362 | require: '?ngMessages',
|
---|
363 | link: function(scope, elem, attr, ngMessages) {
|
---|
364 | if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
---|
365 |
|
---|
366 | if (!elem.attr('aria-live')) {
|
---|
367 | elem.attr('aria-live', 'assertive');
|
---|
368 | }
|
---|
369 | }
|
---|
370 | };
|
---|
371 | })
|
---|
372 | .directive('ngClick',['$aria', '$parse', function($aria, $parse) {
|
---|
373 | return {
|
---|
374 | restrict: 'A',
|
---|
375 | compile: function(elem, attr) {
|
---|
376 | if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
---|
377 |
|
---|
378 | var fn = $parse(attr.ngClick);
|
---|
379 | return function(scope, elem, attr) {
|
---|
380 |
|
---|
381 | if (!isNodeOneOf(elem, nativeAriaNodeNames)) {
|
---|
382 |
|
---|
383 | if ($aria.config('bindRoleForClick') && !elem.attr('role')) {
|
---|
384 | elem.attr('role', 'button');
|
---|
385 | }
|
---|
386 |
|
---|
387 | if ($aria.config('tabindex') && !elem.attr('tabindex')) {
|
---|
388 | elem.attr('tabindex', 0);
|
---|
389 | }
|
---|
390 |
|
---|
391 | if ($aria.config('bindKeydown') && !attr.ngKeydown && !attr.ngKeypress && !attr.ngKeyup) {
|
---|
392 | elem.on('keydown', function(event) {
|
---|
393 | var keyCode = event.which || event.keyCode;
|
---|
394 |
|
---|
395 | if (keyCode === 13 || keyCode === 32) {
|
---|
396 | // If the event is triggered on a non-interactive element ...
|
---|
397 | if (nativeAriaNodeNames.indexOf(event.target.nodeName) === -1 && !event.target.isContentEditable) {
|
---|
398 | // ... prevent the default browser behavior (e.g. scrolling when pressing spacebar)
|
---|
399 | // See https://github.com/angular/angular.js/issues/16664
|
---|
400 | event.preventDefault();
|
---|
401 | }
|
---|
402 | scope.$apply(callback);
|
---|
403 | }
|
---|
404 |
|
---|
405 | function callback() {
|
---|
406 | fn(scope, { $event: event });
|
---|
407 | }
|
---|
408 | });
|
---|
409 | }
|
---|
410 | }
|
---|
411 | };
|
---|
412 | }
|
---|
413 | };
|
---|
414 | }])
|
---|
415 | .directive('ngDblclick', ['$aria', function($aria) {
|
---|
416 | return function(scope, elem, attr) {
|
---|
417 | if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
|
---|
418 |
|
---|
419 | if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nativeAriaNodeNames)) {
|
---|
420 | elem.attr('tabindex', 0);
|
---|
421 | }
|
---|
422 | };
|
---|
423 | }]);
|
---|
424 |
|
---|
425 |
|
---|
426 | })(window, window.angular);
|
---|