Skip to content

Commit 0cc7ca8

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 0cc7ca8

File tree

4 files changed

+743
-4
lines changed

4 files changed

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

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

0 commit comments

Comments
 (0)