Skip to content

Commit ba626cb

Browse files
author
Subra
committed
feat(ngAria): New module to make a11y easier
Adds various aria attributes to the built in directives. This module currently hooks into ng-show/hide, input, textarea button as a basic level of support for a11y. I am using this as a base for adding more tags into the mix for form direction flow, making ng-repeat updates atomic but the tags here are the most basic ones. Closes angular#5486 and angular#1600
1 parent 11f5aee commit ba626cb

File tree

4 files changed

+442
-4
lines changed

4 files changed

+442
-4
lines changed

Gruntfile.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ module.exports = function(grunt) {
147147
},
148148
ngTouch: {
149149
files: { src: 'src/ngTouch/**/*.js' },
150+
},
151+
ngAria: {
152+
files: {src: 'src/ngAria/**/*.js'},
150153
}
151154
},
152155

@@ -214,6 +217,10 @@ module.exports = function(grunt) {
214217
dest: 'build/angular-cookies.js',
215218
src: util.wrap(files['angularModules']['ngCookies'], 'module')
216219
},
220+
aria: {
221+
dest: 'build/angular-aria.js',
222+
src: util.wrap(files['angularModules']['ngAria'], 'module')
223+
},
217224
"promises-aplus-adapter": {
218225
dest:'tmp/promises-aplus-adapter++.js',
219226
src:['src/ng/q.js','lib/promises-aplus/promises-aplus-test-adapter.js']
@@ -230,7 +237,8 @@ module.exports = function(grunt) {
230237
touch: 'build/angular-touch.js',
231238
resource: 'build/angular-resource.js',
232239
route: 'build/angular-route.js',
233-
sanitize: 'build/angular-sanitize.js'
240+
sanitize: 'build/angular-sanitize.js',
241+
aria: 'build/angular-aria.js'
234242
},
235243

236244

angularFiles.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ var angularFiles = {
106106
'src/ngTouch/directive/ngClick.js',
107107
'src/ngTouch/directive/ngSwipe.js'
108108
],
109+
'ngAria': [
110+
'src/ngAria/aria.js'
111+
]
109112
},
110113

111114
'angularScenario': [
@@ -139,7 +142,8 @@ var angularFiles = {
139142
'test/ngRoute/**/*.js',
140143
'test/ngSanitize/**/*.js',
141144
'test/ngMock/*.js',
142-
'test/ngTouch/**/*.js'
145+
'test/ngTouch/**/*.js',
146+
'test/ngAria/*.js'
143147
],
144148

145149
'karma': [
@@ -173,7 +177,8 @@ var angularFiles = {
173177
'test/ngRoute/**/*.js',
174178
'test/ngResource/*.js',
175179
'test/ngSanitize/**/*.js',
176-
'test/ngTouch/**/*.js'
180+
'test/ngTouch/**/*.js',
181+
'test/ngAria/*.js'
177182
],
178183

179184
'karmaJquery': [
@@ -201,7 +206,8 @@ angularFiles['angularSrcModules'] = [].concat(
201206
angularFiles['angularModules']['ngRoute'],
202207
angularFiles['angularModules']['ngSanitize'],
203208
angularFiles['angularModules']['ngMock'],
204-
angularFiles['angularModules']['ngTouch']
209+
angularFiles['angularModules']['ngTouch'],
210+
angularFiles['angularModules']['ngAria']
205211
);
206212

207213
if (exports) {

src/ngAria/aria.js

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
'use strict';
2+
3+
/**
4+
* @ngdoc module
5+
* @name ngAria
6+
* @description
7+
*
8+
* The `ngAria` module provides support for to embed aria tags that convey state or semantic information
9+
* about the application in order to allow assistive technologies to convey appropriate information to
10+
* persons with disabilities.
11+
*
12+
* <div doc-module-components="ngAria"></div>
13+
*
14+
* # Usage
15+
* To enable the addition of the aria tags, just require the module into your application and the tags will
16+
* hook into your ng-show/ng-hide, input, textarea, button, select and ng-required directives and adds the
17+
* appropriate aria-tags.
18+
*
19+
* Currently, the following aria tags are implemented:
20+
*
21+
* + aria-hidden
22+
* + aria-checked
23+
* + aria-disabled
24+
* + aria-required
25+
* + aria-invalid
26+
*
27+
* You can disable individual aria tags by using the {@link ngAria.$ariaProvider#config config} method.
28+
*/
29+
30+
/* global -ngAriaModule */
31+
var ngAriaModule = angular.module('ngAria', ['ng']).
32+
provider('$aria', $AriaProvider);
33+
34+
/**
35+
* @ngdoc provider
36+
* @name $ariaProvider
37+
*
38+
* @description
39+
*
40+
* Used for configuring aria attributes.
41+
*
42+
* ## Dependencies
43+
* Requires the {@link ngAria `ngAria`} module to be installed.
44+
*/
45+
function $AriaProvider(){
46+
var config = {
47+
ariaHidden : true,
48+
ariaChecked: true,
49+
ariaDisabled: true,
50+
ariaRequired: true,
51+
ariaInvalid: true
52+
};
53+
54+
/**
55+
* @ngdoc method
56+
* @name $ariaProvider#config
57+
*
58+
* @param {object} config object to enable/disable specific aria tags
59+
*
60+
* - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags
61+
* - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags
62+
* - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags
63+
* - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags
64+
* - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags
65+
*
66+
* @description
67+
* Enables/disables various aria tags
68+
*/
69+
this.config = function(newConfig){
70+
config = angular.extend(config, newConfig);
71+
};
72+
73+
var convertCase = function(input){
74+
return input.replace(/[A-Z]/g, function(letter, pos){
75+
return (pos ? '-' : '') + letter.toLowerCase();
76+
});
77+
};
78+
79+
var watchAttr = function(attrName, ariaName){
80+
return function(scope, elem, attr){
81+
if(config[ariaName]){
82+
if(attr[attrName]){
83+
elem.attr(convertCase(ariaName), true);
84+
}
85+
var destroyWatcher = attr.$observe(attrName, function(newVal){
86+
elem.attr(convertCase(ariaName), !angular.isUndefined(newVal));
87+
});
88+
scope.$on('$destroy', function(){
89+
destroyWatcher();
90+
});
91+
}
92+
};
93+
};
94+
95+
var watchClass = function(className, ariaName){
96+
return function(scope, elem, attr){
97+
if(config[ariaName]){
98+
var destroyWatcher = scope.$watch(function(){
99+
return elem.attr('class');
100+
}, function(){
101+
elem.attr(convertCase(ariaName), elem.hasClass(className));
102+
});
103+
scope.$on('$destroy', function(){
104+
destroyWatcher();
105+
});
106+
}
107+
};
108+
};
109+
110+
var watchExpr = function(expr, ariaName){
111+
return function(scope, elem, attr){
112+
if(config[ariaName]){
113+
var destroyWatch;
114+
var destroyObserve = attr.$observe(expr, function(value){
115+
if(angular.isFunction(destroyWatch)){
116+
destroyWatch();
117+
}
118+
destroyWatch = scope.$watch(value, function(newVal){
119+
elem.attr(convertCase(ariaName), newVal);
120+
});
121+
});
122+
scope.$on('$destroy', function(){
123+
destroyObserve();
124+
});
125+
}
126+
};
127+
};
128+
129+
this.$get = function(){
130+
return {
131+
ariaHidden: watchClass('ng-hide', 'ariaHidden'),
132+
ariaChecked: watchExpr('ngModel', 'ariaChecked'),
133+
ariaDisabled: watchExpr('ngDisabled', 'ariaDisabled'),
134+
ariaNgRequired: watchExpr('ngRequired', 'ariaRequired'),
135+
ariaRequired: watchAttr('required', 'ariaRequired'),
136+
ariaInvalid: watchClass('ng-invalid', 'ariaInvalid')
137+
};
138+
};
139+
}
140+
141+
ngAriaModule.directive('ngShow', ['$aria', function($aria){
142+
return $aria.ariaHidden;
143+
}]).directive('ngHide', ['$aria', function($aria){
144+
return $aria.ariaHidden;
145+
}]).directive('input', ['$aria', function($aria){
146+
return{
147+
restrict: 'E',
148+
require: '?ngModel',
149+
link: function(scope, elem, attr, ngModel){
150+
if(attr.type === 'checkbox'){
151+
$aria.ariaChecked(scope, elem, attr);
152+
}
153+
if(attr.type === 'radio' && ngModel){
154+
scope.$watch(function(){
155+
return ngModel.$modelValue;
156+
}, function(newVal){
157+
if(newVal === attr.value){
158+
elem.attr('aria-checked', true);
159+
}else{
160+
elem.attr('aria-checked', false);
161+
}
162+
});
163+
}
164+
$aria.ariaRequired(scope, elem, attr);
165+
$aria.ariaInvalid(scope, elem, attr);
166+
}
167+
};
168+
}]).directive('textarea', ['$aria', function($aria){
169+
return{
170+
restrict: 'E',
171+
link: function(scope, elem, attr){
172+
$aria.ariaRequired(scope, elem, attr);
173+
$aria.ariaInvalid(scope, elem, attr);
174+
}
175+
};
176+
}]).directive('select', ['$aria', function($aria){
177+
return{
178+
restrict: 'E',
179+
link: function(scope, elem, attr){
180+
$aria.ariaRequired(scope, elem, attr);
181+
}
182+
};
183+
}])
184+
.directive('ngRequired', ['$aria', function($aria){
185+
return $aria.ariaNgRequired;
186+
}])
187+
.directive('ngDisabled', ['$aria', function($aria){
188+
return $aria.ariaDisabled;
189+
}]);

0 commit comments

Comments
 (0)