Skip to content

Commit 88e09ee

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 88e09ee

File tree

4 files changed

+604
-4
lines changed

4 files changed

+604
-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

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
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+
ariaMultiline: true,
53+
ariaValue: true
54+
};
55+
56+
/**
57+
* @ngdoc method
58+
* @name $ariaProvider#config
59+
*
60+
* @param {object} config object to enable/disable specific aria tags
61+
*
62+
* - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags
63+
* - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags
64+
* - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags
65+
* - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags
66+
* - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags
67+
* - **ariaMultiline** – `{boolean}` – Enables/disables aria-multiline tags
68+
* - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags
69+
*
70+
* @description
71+
* Enables/disables various aria tags
72+
*/
73+
this.config = function(newConfig){
74+
config = angular.extend(config, newConfig);
75+
};
76+
77+
var convertCase = function(input){
78+
return input.replace(/[A-Z]/g, function(letter, pos){
79+
return (pos ? '-' : '') + letter.toLowerCase();
80+
});
81+
};
82+
83+
var watchAttr = function(attrName, ariaName){
84+
return function(scope, elem, attr){
85+
if(config[ariaName]){
86+
if(attr[attrName]){
87+
elem.attr(convertCase(ariaName), true);
88+
}
89+
var destroyWatcher = attr.$observe(attrName, function(newVal){
90+
elem.attr(convertCase(ariaName), !angular.isUndefined(newVal));
91+
});
92+
scope.$on('$destroy', function(){
93+
destroyWatcher();
94+
});
95+
}
96+
};
97+
};
98+
99+
var watchClass = function(className, ariaName){
100+
return function(scope, elem, attr){
101+
if(config[ariaName]){
102+
var destroyWatcher = scope.$watch(function(){
103+
return elem.attr('class');
104+
}, function(){
105+
elem.attr(convertCase(ariaName), elem.hasClass(className));
106+
});
107+
scope.$on('$destroy', function(){
108+
destroyWatcher();
109+
});
110+
}
111+
};
112+
};
113+
114+
var watchExpr = function(expr, ariaName){
115+
return function(scope, elem, attr){
116+
if(config[ariaName]){
117+
var destroyWatch;
118+
var destroyObserve = attr.$observe(expr, function(value){
119+
if(angular.isFunction(destroyWatch)){
120+
destroyWatch();
121+
}
122+
destroyWatch = scope.$watch(value, function(newVal){
123+
elem.attr(convertCase(ariaName), newVal);
124+
});
125+
});
126+
scope.$on('$destroy', function(){
127+
destroyObserve();
128+
});
129+
}
130+
};
131+
};
132+
133+
this.$get = function(){
134+
return {
135+
ariaHidden: watchClass('ng-hide', 'ariaHidden'),
136+
ariaChecked: watchExpr('ngModel', 'ariaChecked'),
137+
ariaDisabled: watchExpr('ngDisabled', 'ariaDisabled'),
138+
ariaNgRequired: watchExpr('ngRequired', 'ariaRequired'),
139+
ariaRequired: watchAttr('required', 'ariaRequired'),
140+
ariaInvalid: watchClass('ng-invalid', 'ariaInvalid'),
141+
ariaValue: function(scope, elem, attr, ngModel){
142+
if(config.ariaValue){
143+
if(attr.min){
144+
elem.attr('aria-valuemin', attr.min);
145+
}
146+
if(attr.max){
147+
elem.attr('aria-valuemax', attr.max);
148+
}
149+
if(ngModel){
150+
var destroyWatcher = scope.$watch(function(){
151+
return ngModel.$modelValue;
152+
}, function(newVal){
153+
elem.attr('aria-valuenow', newVal);
154+
});
155+
scope.$on('$destroy', function(){
156+
destroyWatcher();
157+
});
158+
}
159+
}
160+
},
161+
radio: function(scope, elem, attr, ngModel){
162+
if(config.ariaChecked && ngModel){
163+
var destroyWatcher = scope.$watch(function(){
164+
return ngModel.$modelValue;
165+
}, function(newVal){
166+
if(newVal === attr.value){
167+
elem.attr('aria-checked', true);
168+
}else{
169+
elem.attr('aria-checked', false);
170+
}
171+
});
172+
scope.$on('$destroy', function(){
173+
destroyWatcher();
174+
});
175+
}
176+
},
177+
multiline: function(scope, elem, attr){
178+
if(config.ariaMultiline){
179+
elem.attr('aria-multiline', true);
180+
}
181+
},
182+
roleChecked: function(scope, elem, attr){
183+
if(config.ariaChecked && attr.checked){
184+
elem.attr('aria-checked', true);
185+
}
186+
}
187+
};
188+
};
189+
}
190+
191+
ngAriaModule.directive('ngShow', ['$aria', function($aria){
192+
return $aria.ariaHidden;
193+
}]).directive('ngHide', ['$aria', function($aria){
194+
return $aria.ariaHidden;
195+
}]).directive('input', ['$aria', function($aria){
196+
return{
197+
restrict: 'E',
198+
require: '?ngModel',
199+
link: function(scope, elem, attr, ngModel){
200+
if(attr.type === 'checkbox'){
201+
$aria.ariaChecked(scope, elem, attr);
202+
}
203+
if(attr.type === 'radio'){
204+
$aria.radio(scope, elem, attr, ngModel);
205+
}
206+
$aria.ariaRequired(scope, elem, attr);
207+
$aria.ariaInvalid(scope, elem, attr);
208+
if(attr.type === 'range'){
209+
$aria.ariaValue(scope, elem, attr, ngModel);
210+
}
211+
}
212+
};
213+
}]).directive('textarea', ['$aria', function($aria){
214+
return{
215+
restrict: 'E',
216+
link: function(scope, elem, attr){
217+
$aria.ariaRequired(scope, elem, attr);
218+
$aria.ariaInvalid(scope, elem, attr);
219+
$aria.multiline(scope, elem, attr);
220+
}
221+
};
222+
}]).directive('select', ['$aria', function($aria){
223+
return{
224+
restrict: 'E',
225+
link: function(scope, elem, attr){
226+
$aria.ariaRequired(scope, elem, attr);
227+
}
228+
};
229+
}])
230+
.directive('ngRequired', ['$aria', function($aria){
231+
return $aria.ariaNgRequired;
232+
}])
233+
.directive('ngDisabled', ['$aria', function($aria){
234+
return $aria.ariaDisabled;
235+
}])
236+
.directive('role', ['$aria', function($aria){
237+
return{
238+
restrict: 'A',
239+
require: '?ngModel',
240+
link: function(scope, elem, attr, ngModel){
241+
if(attr.role === 'textbox'){
242+
$aria.multiline(scope, elem, attr);
243+
}
244+
if(attr.role === "progressbar" || attr.role === "slider"){
245+
$aria.ariaValue(scope, elem, attr, ngModel);
246+
}
247+
if(attr.role === "checkbox" || attr.role === "menuitemcheckbox"){
248+
$aria.roleChecked(scope, elem, attr);
249+
}
250+
if(attr.role === "radio" || attr.role === "menuitemradio"){
251+
$aria.radio(scope, elem, attr, ngModel);
252+
}
253+
}
254+
};
255+
}]);

0 commit comments

Comments
 (0)