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