1 | /** |
||
2 | * @license AngularJS v1.8.3 |
||
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.3' }). |
||
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; |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
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; |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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); |
||
427 |