|
| 1 | +/** |
| 2 | + * @license AngularJS v1.3.3 |
| 3 | + * (c) 2010-2014 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 as a dependency. The directives supported |
| 23 | + * by ngAria are: |
| 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:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required | |
| 31 | + * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled | |
| 32 | + * | {@link ng.directive:ngShow ngShow} | aria-hidden | |
| 33 | + * | {@link ng.directive:ngHide ngHide} | aria-hidden | |
| 34 | + * | {@link ng.directive:ngClick ngClick} | tabindex | |
| 35 | + * | {@link ng.directive:ngDblclick ngDblclick} | tabindex | |
| 36 | + * | {@link module:ngMessages ngMessages} | aria-live | |
| 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 | + * @ngdoc provider |
| 62 | + * @name $ariaProvider |
| 63 | + * |
| 64 | + * @description |
| 65 | + * |
| 66 | + * Used for configuring the ARIA attributes injected and managed by ngAria. |
| 67 | + * |
| 68 | + * ```js |
| 69 | + * angular.module('myApp', ['ngAria'], function config($ariaProvider) { |
| 70 | + * $ariaProvider.config({ |
| 71 | + * ariaValue: true, |
| 72 | + * tabindex: false |
| 73 | + * }); |
| 74 | + * }); |
| 75 | + *``` |
| 76 | + * |
| 77 | + * ## Dependencies |
| 78 | + * Requires the {@link ngAria} module to be installed. |
| 79 | + * |
| 80 | + */ |
| 81 | +function $AriaProvider() { |
| 82 | + var config = { |
| 83 | + ariaHidden: true, |
| 84 | + ariaChecked: true, |
| 85 | + ariaDisabled: true, |
| 86 | + ariaRequired: true, |
| 87 | + ariaInvalid: true, |
| 88 | + ariaMultiline: true, |
| 89 | + ariaValue: true, |
| 90 | + tabindex: true |
| 91 | + }; |
| 92 | + |
| 93 | + /** |
| 94 | + * @ngdoc method |
| 95 | + * @name $ariaProvider#config |
| 96 | + * |
| 97 | + * @param {object} config object to enable/disable specific ARIA attributes |
| 98 | + * |
| 99 | + * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags |
| 100 | + * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags |
| 101 | + * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags |
| 102 | + * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags |
| 103 | + * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags |
| 104 | + * - **ariaMultiline** – `{boolean}` – Enables/disables aria-multiline tags |
| 105 | + * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags |
| 106 | + * - **tabindex** – `{boolean}` – Enables/disables tabindex tags |
| 107 | + * |
| 108 | + * @description |
| 109 | + * Enables/disables various ARIA attributes |
| 110 | + */ |
| 111 | + this.config = function(newConfig) { |
| 112 | + config = angular.extend(config, newConfig); |
| 113 | + }; |
| 114 | + |
| 115 | + function camelCase(input) { |
| 116 | + return input.replace(/-./g, function(letter, pos) { |
| 117 | + return letter[1].toUpperCase(); |
| 118 | + }); |
| 119 | + } |
| 120 | + |
| 121 | + |
| 122 | + function watchExpr(attrName, ariaAttr, negate) { |
| 123 | + var ariaCamelName = camelCase(ariaAttr); |
| 124 | + return function(scope, elem, attr) { |
| 125 | + if (config[ariaCamelName] && !attr[ariaCamelName]) { |
| 126 | + scope.$watch(attr[attrName], function(boolVal) { |
| 127 | + if (negate) { |
| 128 | + boolVal = !boolVal; |
| 129 | + } |
| 130 | + elem.attr(ariaAttr, boolVal); |
| 131 | + }); |
| 132 | + } |
| 133 | + }; |
| 134 | + } |
| 135 | + |
| 136 | + /** |
| 137 | + * @ngdoc service |
| 138 | + * @name $aria |
| 139 | + * |
| 140 | + * @description |
| 141 | + * |
| 142 | + * The $aria service contains helper methods for applying common |
| 143 | + * [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives. |
| 144 | + * |
| 145 | + * ngAria injects common accessibility attributes that tell assistive technologies when HTML |
| 146 | + * elements are enabled, selected, hidden, and more. To see how this is performed with ngAria, |
| 147 | + * let's review a code snippet from ngAria itself: |
| 148 | + * |
| 149 | + *```js |
| 150 | + * ngAriaModule.directive('ngDisabled', ['$aria', function($aria) { |
| 151 | + * return $aria.$$watchExpr('ngDisabled', 'aria-disabled'); |
| 152 | + * }]) |
| 153 | + *``` |
| 154 | + * Shown above, the ngAria module creates a directive with the same signature as the |
| 155 | + * traditional `ng-disabled` directive. But this ngAria version is dedicated to |
| 156 | + * solely managing accessibility attributes. The internal `$aria` service is used to watch the |
| 157 | + * boolean attribute `ngDisabled`. If it has not been explicitly set by the developer, |
| 158 | + * `aria-disabled` is injected as an attribute with its value synchronized to the value in |
| 159 | + * `ngDisabled`. |
| 160 | + * |
| 161 | + * Because ngAria hooks into the `ng-disabled` directive, developers do not have to do |
| 162 | + * anything to enable this feature. The `aria-disabled` attribute is automatically managed |
| 163 | + * simply as a silent side-effect of using `ng-disabled` with the ngAria module. |
| 164 | + * |
| 165 | + * The full list of directives that interface with ngAria: |
| 166 | + * * **ngModel** |
| 167 | + * * **ngShow** |
| 168 | + * * **ngHide** |
| 169 | + * * **ngClick** |
| 170 | + * * **ngDblclick** |
| 171 | + * * **ngMessages** |
| 172 | + * * **ngDisabled** |
| 173 | + * |
| 174 | + * Read the {@link guide/accessibility ngAria Developer Guide} for a thorough explanation of each |
| 175 | + * directive. |
| 176 | + * |
| 177 | + * |
| 178 | + * ## Dependencies |
| 179 | + * Requires the {@link ngAria} module to be installed. |
| 180 | + */ |
| 181 | + this.$get = function() { |
| 182 | + return { |
| 183 | + config: function(key) { |
| 184 | + return config[camelCase(key)]; |
| 185 | + }, |
| 186 | + $$watchExpr: watchExpr |
| 187 | + }; |
| 188 | + }; |
| 189 | +} |
| 190 | + |
| 191 | +var ngAriaTabindex = ['$aria', function($aria) { |
| 192 | + return function(scope, elem, attr) { |
| 193 | + if ($aria.config('tabindex') && !elem.attr('tabindex')) { |
| 194 | + elem.attr('tabindex', 0); |
| 195 | + } |
| 196 | + }; |
| 197 | +}]; |
| 198 | + |
| 199 | +ngAriaModule.directive('ngShow', ['$aria', function($aria) { |
| 200 | + return $aria.$$watchExpr('ngShow', 'aria-hidden', true); |
| 201 | +}]) |
| 202 | +.directive('ngHide', ['$aria', function($aria) { |
| 203 | + return $aria.$$watchExpr('ngHide', 'aria-hidden', false); |
| 204 | +}]) |
| 205 | +.directive('ngModel', ['$aria', function($aria) { |
| 206 | + |
| 207 | + function shouldAttachAttr(attr, elem) { |
| 208 | + return $aria.config(attr) && !elem.attr(attr); |
| 209 | + } |
| 210 | + |
| 211 | + function getShape(attr, elem) { |
| 212 | + var type = attr.type, |
| 213 | + role = attr.role; |
| 214 | + |
| 215 | + return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' : |
| 216 | + ((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' : |
| 217 | + (type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' : |
| 218 | + (type || role) === 'textbox' || elem[0].nodeName === 'TEXTAREA' ? 'multiline' : ''; |
| 219 | + } |
| 220 | + |
| 221 | + return { |
| 222 | + restrict: 'A', |
| 223 | + require: '?ngModel', |
| 224 | + link: function(scope, elem, attr, ngModel) { |
| 225 | + var shape = getShape(attr, elem); |
| 226 | + var needsTabIndex = shouldAttachAttr('tabindex', elem); |
| 227 | + |
| 228 | + function ngAriaWatchModelValue() { |
| 229 | + return ngModel.$modelValue; |
| 230 | + } |
| 231 | + |
| 232 | + function getRadioReaction() { |
| 233 | + if (needsTabIndex) { |
| 234 | + needsTabIndex = false; |
| 235 | + return function ngAriaRadioReaction(newVal) { |
| 236 | + var boolVal = newVal === attr.value; |
| 237 | + elem.attr('aria-checked', boolVal); |
| 238 | + elem.attr('tabindex', 0 - !boolVal); |
| 239 | + }; |
| 240 | + } else { |
| 241 | + return function ngAriaRadioReaction(newVal) { |
| 242 | + elem.attr('aria-checked', newVal === attr.value); |
| 243 | + }; |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + function ngAriaCheckboxReaction(newVal) { |
| 248 | + elem.attr('aria-checked', !!newVal); |
| 249 | + } |
| 250 | + |
| 251 | + switch (shape) { |
| 252 | + case 'radio': |
| 253 | + case 'checkbox': |
| 254 | + if (shouldAttachAttr('aria-checked', elem)) { |
| 255 | + scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? |
| 256 | + getRadioReaction() : ngAriaCheckboxReaction); |
| 257 | + } |
| 258 | + break; |
| 259 | + case 'range': |
| 260 | + if ($aria.config('ariaValue')) { |
| 261 | + if (attr.min && !elem.attr('aria-valuemin')) { |
| 262 | + elem.attr('aria-valuemin', attr.min); |
| 263 | + } |
| 264 | + if (attr.max && !elem.attr('aria-valuemax')) { |
| 265 | + elem.attr('aria-valuemax', attr.max); |
| 266 | + } |
| 267 | + if (!elem.attr('aria-valuenow')) { |
| 268 | + scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { |
| 269 | + elem.attr('aria-valuenow', newVal); |
| 270 | + }); |
| 271 | + } |
| 272 | + } |
| 273 | + break; |
| 274 | + case 'multiline': |
| 275 | + if (shouldAttachAttr('aria-multiline', elem)) { |
| 276 | + elem.attr('aria-multiline', true); |
| 277 | + } |
| 278 | + break; |
| 279 | + } |
| 280 | + |
| 281 | + if (needsTabIndex) { |
| 282 | + elem.attr('tabindex', 0); |
| 283 | + } |
| 284 | + |
| 285 | + if (ngModel.$validators.required && shouldAttachAttr('aria-required', elem)) { |
| 286 | + scope.$watch(function ngAriaRequiredWatch() { |
| 287 | + return ngModel.$error.required; |
| 288 | + }, function ngAriaRequiredReaction(newVal) { |
| 289 | + elem.attr('aria-required', !!newVal); |
| 290 | + }); |
| 291 | + } |
| 292 | + |
| 293 | + if (shouldAttachAttr('aria-invalid', elem)) { |
| 294 | + scope.$watch(function ngAriaInvalidWatch() { |
| 295 | + return ngModel.$invalid; |
| 296 | + }, function ngAriaInvalidReaction(newVal) { |
| 297 | + elem.attr('aria-invalid', !!newVal); |
| 298 | + }); |
| 299 | + } |
| 300 | + } |
| 301 | + }; |
| 302 | +}]) |
| 303 | +.directive('ngDisabled', ['$aria', function($aria) { |
| 304 | + return $aria.$$watchExpr('ngDisabled', 'aria-disabled'); |
| 305 | +}]) |
| 306 | +.directive('ngMessages', function() { |
| 307 | + return { |
| 308 | + restrict: 'A', |
| 309 | + require: '?ngMessages', |
| 310 | + link: function(scope, elem, attr, ngMessages) { |
| 311 | + if (!elem.attr('aria-live')) { |
| 312 | + elem.attr('aria-live', 'assertive'); |
| 313 | + } |
| 314 | + } |
| 315 | + }; |
| 316 | +}) |
| 317 | +.directive('ngClick', ngAriaTabindex) |
| 318 | +.directive('ngDblclick', ngAriaTabindex); |
| 319 | + |
| 320 | + |
| 321 | +})(window, window.angular); |
0 commit comments