Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0cc7ca8

Browse files
author
Subra
committedSep 11, 2014
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+
}]);

‎test/ngAria/ariaSpec.js

+444
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,444 @@
1+
'use strict';
2+
3+
function expectAriaAttr(elem, ariaAttr, expected){
4+
angular.forEach(elem, function(val){
5+
expect(angular.element(val).attr(ariaAttr)).toBe(expected);
6+
});
7+
}
8+
9+
describe('$aria', function(){
10+
11+
describe('aria-hidden', function(){
12+
beforeEach(module('ngAria'));
13+
14+
it('should attach aria-hidden to ng-show', inject(function($compile, $rootScope){
15+
var element = $compile("<div ng-show='val'></div>")($rootScope);
16+
$rootScope.$apply('val=false');
17+
expectAriaAttr(element, 'aria-hidden', 'true');
18+
$rootScope.$apply('val=true');
19+
expectAriaAttr(element, 'aria-hidden', 'false');
20+
}));
21+
22+
it('should attach aria-hidden to ng-hide', inject(function($compile, $rootScope){
23+
var element = $compile("<div ng-hide='val'></div>")($rootScope);
24+
$rootScope.$apply('val=false');
25+
expectAriaAttr(element, 'aria-hidden', 'false');
26+
$rootScope.$apply('val=true');
27+
expectAriaAttr(element, 'aria-hidden', 'true');
28+
}));
29+
30+
it('should not attach if an aria-hidden is already present', inject(function($compile, $rootScope){
31+
var element = [
32+
$compile('<div ng-show="val" aria-hidden="userSetValue"></div>')($rootScope),
33+
$compile('<div ng-hide="val" aria-hidden="userSetValue"></div>')($rootScope)
34+
];
35+
$rootScope.$apply('val=true');
36+
expectAriaAttr(element, 'aria-hidden', 'userSetValue');
37+
}));
38+
39+
describe('disabled', function(){
40+
beforeEach(function(){
41+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
42+
$ariaProvider.config({
43+
ariaHidden : false
44+
});
45+
});
46+
47+
module('ariaTest');
48+
});
49+
50+
it('should not attach aria-hidden if the option is disabled', inject(function($compile, $rootScope){
51+
var element = [
52+
$compile("<div ng-show='val'></div>")($rootScope),
53+
$compile("<div ng-hide='val'></div>")($rootScope)
54+
];
55+
$rootScope.$apply('val=false');
56+
expectAriaAttr(element, 'aria-hidden', undefined);
57+
}));
58+
});
59+
});
60+
61+
describe('aria-checked', function(){
62+
beforeEach(module('ngAria'));
63+
64+
it('should attach itself to input type=checkbox', inject(function($compile, $rootScope){
65+
var element = $compile("<input type='checkbox' ng-model='val' ng-init='val = true'>")($rootScope);
66+
$rootScope.$apply('val=true');
67+
expectAriaAttr(element, 'aria-checked', 'true');
68+
$rootScope.$apply('val=false');
69+
expectAriaAttr(element, 'aria-checked', 'false');
70+
}));
71+
72+
it('should attach itself to input type=radio', inject(function($compile, $rootScope){
73+
var element = $compile("<input type='radio' ng-model='val' value='one'><input type='radio' ng-model='val' value='two'>")($rootScope);
74+
$rootScope.$apply("val='one'");
75+
expect(angular.element(element).eq(0).attr('aria-checked')).toBe('true');
76+
expect(angular.element(element).eq(1).attr('aria-checked')).toBe('false');
77+
78+
$rootScope.$apply("val='two'");
79+
expect(angular.element(element).eq(0).attr('aria-checked')).toBe('false');
80+
expect(angular.element(element).eq(1).attr('aria-checked')).toBe('true');
81+
}));
82+
83+
it('should attach itself to role="radio", role="checkbox", role="menuitemradio" and role="menuitemcheckbox"', inject(function($compile, $rootScope){
84+
var element = [
85+
$compile("<div role='radio' ng-model='val' value='{{val}}'></div>")($rootScope),
86+
$compile("<div role='menuitemradio' ng-model='val' value='{{val}}'></div>")($rootScope),
87+
$compile("<div role='checkbox' checked='checked'></div>")($rootScope),
88+
$compile("<div role='menuitemcheckbox' checked='checked'></div>")($rootScope)
89+
];
90+
$rootScope.$apply("val='one'");
91+
expectAriaAttr(element, 'aria-checked', 'true');
92+
}));
93+
94+
it('should not attach itself if an aria-checked value is already present', inject(function($compile, $rootScope){
95+
var element = [
96+
$compile("<input type='checkbox' ng-model='val1' aria-checked='userSetValue'>")($rootScope),
97+
$compile("<input type='radio' ng-model='val2' value='one' aria-checked='userSetValue'><input type='radio' ng-model='val2' value='two'>")($rootScope),
98+
$compile("<div role='radio' ng-model='val' value='{{val3}}' aria-checked='userSetValue'></div>")($rootScope),
99+
$compile("<div role='menuitemradio' ng-model='val' value='{{val3}}' aria-checked='userSetValue'></div>")($rootScope),
100+
$compile("<div role='checkbox' checked='checked' aria-checked='userSetValue'></div>")($rootScope),
101+
$compile("<div role='menuitemcheckbox' checked='checked' aria-checked='userSetValue'></div>")($rootScope)
102+
];
103+
$rootScope.$apply("val1=true;val2='one';val3='1'");
104+
expectAriaAttr(element, 'aria-checked', 'userSetValue');
105+
}));
106+
107+
describe('disabled', function(){
108+
beforeEach(function(){
109+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
110+
$ariaProvider.config({
111+
ariaChecked : false
112+
});
113+
});
114+
115+
module('ariaTest');
116+
});
117+
118+
it('should not attach aria-checked if the option is disabled', inject(function($compile, $rootScope){
119+
var element = [
120+
$compile("<div role='radio' ng-model='val' value='{{val}}'></div>")($rootScope),
121+
$compile("<div role='menuitemradio' ng-model='val' value='{{val}}'></div>")($rootScope),
122+
$compile("<div role='checkbox' checked='checked'></div>")($rootScope),
123+
$compile("<div role='menuitemcheckbox' checked='checked'></div>")($rootScope)
124+
];
125+
$rootScope.$digest();
126+
expectAriaAttr(element, 'aria-checked', undefined);
127+
}));
128+
});
129+
});
130+
131+
describe('aria-disabled', function(){
132+
beforeEach(module('ngAria'));
133+
134+
it('should attach itself to input, textarea, button and select', inject(function($compile, $rootScope){
135+
var element = [
136+
$compile("<input ng-disabled='val'>")($rootScope),
137+
$compile("<textarea ng-disabled='val'></textarea>")($rootScope),
138+
$compile("<button ng-disabled='val'></button>")($rootScope),
139+
$compile("<select ng-disabled='val'></select>")($rootScope)
140+
];
141+
$rootScope.$apply('val=false');
142+
expectAriaAttr(element, 'aria-disabled', 'false');
143+
144+
$rootScope.$apply('val=true');
145+
expectAriaAttr(element, 'aria-disabled', 'true');
146+
}));
147+
148+
it('should not attach itself if an aria tag is already present', inject(function($compile, $rootScope){
149+
var element = [
150+
$compile("<input aria-disabled='userSetValue' ng-disabled='val'>")($rootScope),
151+
$compile("<textarea aria-disabled='userSetValue' ng-disabled='val'></textarea>")($rootScope),
152+
$compile("<button aria-disabled='userSetValue' ng-disabled='val'></button>")($rootScope),
153+
$compile("<select aria-disabled='userSetValue' ng-disabled='val'></select>")($rootScope)
154+
];
155+
156+
$rootScope.$apply('val=true');
157+
expectAriaAttr(element, 'aria-disabled', 'userSetValue');
158+
}));
159+
160+
describe('disabled', function(){
161+
beforeEach(function(){
162+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
163+
$ariaProvider.config({
164+
ariaDisabled : false
165+
});
166+
});
167+
168+
module('ariaTest');
169+
});
170+
171+
it('should not attach aria-disabled if the option is disabled', inject(function($compile, $rootScope){
172+
var element = [
173+
$compile("<input ng-disabled='val'>")($rootScope),
174+
$compile("<textarea ng-disabled='val'></textarea>")($rootScope),
175+
$compile("<button ng-disabled='val'></button>")($rootScope),
176+
$compile("<select ng-disabled='val'></select>")($rootScope)
177+
];
178+
179+
$rootScope.$apply('val=false');
180+
expectAriaAttr(element, 'aria-disabled', undefined);
181+
}));
182+
});
183+
});
184+
185+
describe('aria-invalid', function(){
186+
beforeEach(module('ngAria'));
187+
188+
it('should attach aria-invalid to input', inject(function($compile, $rootScope){
189+
var element = $compile("<input ng-model='txtInput' ng-minlength='10'>")($rootScope);
190+
$rootScope.$apply("txtInput='LTten'");
191+
expectAriaAttr(element, 'aria-invalid', 'true');
192+
193+
$rootScope.$apply("txtInput='morethantencharacters'");
194+
expectAriaAttr(element, 'aria-invalid', 'false');
195+
}));
196+
197+
it('should not attach itself if aria-invalid is already present', inject(function($compile, $rootScope){
198+
var element = $compile("<input ng-model='txtInput' ng-minlength='10' aria-invalid='userSetValue'>")($rootScope);
199+
$rootScope.$apply("txtInput='LTten'");
200+
expectAriaAttr(element, 'aria-invalid', 'userSetValue');
201+
}));
202+
203+
describe('disabled', function(){
204+
beforeEach(function(){
205+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
206+
$ariaProvider.config({
207+
ariaInvalid : false
208+
});
209+
});
210+
211+
module('ariaTest');
212+
});
213+
214+
it('should not attach aria-invalid if the option is disabled', inject(function($compile, $rootScope){
215+
var element = $compile("<input ng-model='txtInput' ng-minlength='10'>")($rootScope);
216+
$rootScope.$apply("txtInput='LTten'");
217+
expectAriaAttr(element, 'aria-invalid', undefined);
218+
}));
219+
});
220+
});
221+
222+
describe('aria-required', function(){
223+
beforeEach(module('ngAria'));
224+
225+
it('should attach aria-required to input, textarea, select and ngRequired', inject(function($compile, $rootScope){
226+
var element = [
227+
$compile("<input ng-model='val' required>")($rootScope),
228+
$compile("<textarea ng-model='val' required></textarea>")($rootScope),
229+
$compile("<select ng-model='val' required></select>")($rootScope),
230+
$compile("<input ng-model='val' ng-required='true'>")($rootScope)
231+
];
232+
$rootScope.$digest();
233+
expectAriaAttr(element, 'aria-required', 'true');
234+
235+
element = [
236+
$compile("<input ng-model='val'>")($rootScope),
237+
$compile("<textarea ng-model='val'></textarea>")($rootScope),
238+
$compile("<select ng-model='val'></select>")($rootScope),
239+
$compile("<input ng-model='val' ng-required='false'>")($rootScope)
240+
];
241+
$rootScope.$apply("val='input is valid now'");
242+
expectAriaAttr(element, 'aria-required', 'false');
243+
}));
244+
245+
it('should not attach itself if aria-required is already present', inject(function($compile, $rootScope){
246+
var element = [
247+
$compile("<input ng-model='val' required aria-required='userSetValue'>")($rootScope),
248+
$compile("<textarea ng-model='val' required aria-required='userSetValue'></textarea>")($rootScope),
249+
$compile("<select ng-model='val' required aria-required='userSetValue'></select>")($rootScope),
250+
$compile("<input ng-model='val' ng-required='true' aria-required='userSetValue'>")($rootScope)
251+
];
252+
253+
$rootScope.$digest();
254+
expectAriaAttr(element, 'aria-required', 'userSetValue');
255+
}));
256+
257+
describe('disabled', function(){
258+
beforeEach(function(){
259+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
260+
$ariaProvider.config({
261+
ariaRequired : false
262+
});
263+
});
264+
265+
module('ariaTest');
266+
});
267+
268+
it('should not attach aria-required when the option is disabled', inject(function($compile, $rootScope){
269+
var element = [
270+
$compile("<input ng-model='val' required>")($rootScope),
271+
$compile("<textarea ng-model='val' required></textarea>")($rootScope),
272+
$compile("<select ng-model='val' required></select>")($rootScope)
273+
];
274+
275+
$rootScope.$digest();
276+
expectAriaAttr(element, 'aria-required', undefined);
277+
}));
278+
});
279+
});
280+
281+
describe('aria-multiline', function(){
282+
beforeEach(module('ngAria'));
283+
284+
it('should attach aria-multiline to textbox and role="textbox"', inject(function($compile, $rootScope){
285+
var element = [
286+
$compile("<textarea></textarea>")($rootScope),
287+
$compile("<div role='textbox'></div>")($rootScope)
288+
];
289+
290+
$rootScope.$digest();
291+
expectAriaAttr(element, 'aria-multiline', 'true');
292+
}));
293+
294+
it('should not attach if aria-multiline is already present', inject(function($compile, $rootScope){
295+
var element = [
296+
$compile("<textarea aria-multiline='userSetValue'></textarea>")($rootScope),
297+
$compile("<div role='textbox' aria-multiline='userSetValue'></div>")($rootScope)
298+
];
299+
300+
$rootScope.$digest();
301+
expectAriaAttr(element, 'aria-multiline', 'userSetValue');
302+
}));
303+
304+
describe('disabled', function(){
305+
beforeEach(function(){
306+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
307+
$ariaProvider.config({
308+
ariaMultiline : false
309+
});
310+
});
311+
312+
module('ariaTest');
313+
});
314+
315+
it('should not attach itself to textbox or role="textbox" when disabled', inject(function($compile, $rootScope){
316+
var element = [
317+
$compile("<textarea></textarea>")($rootScope),
318+
$compile("<div role='textbox'></div>")($rootScope)
319+
];
320+
321+
$rootScope.$digest();
322+
expectAriaAttr(element, 'aria-multiline', undefined);
323+
}));
324+
});
325+
});
326+
327+
describe('aria-value', function(){
328+
beforeEach(module('ngAria'));
329+
330+
it('should attach to input type="range"', inject(function($compile, $rootScope){
331+
var element = [
332+
$compile('<input type="range" ng-model="val" min="0" max="100">')($rootScope),
333+
$compile('<div role="progressbar" min="0" max="100" ng-model="val">')($rootScope),
334+
$compile('<div role="slider" min="0" max="100" ng-model="val">')($rootScope)
335+
];
336+
337+
$rootScope.$apply('val=50');
338+
expectAriaAttr(element, 'aria-valuenow', "50");
339+
expectAriaAttr(element, 'aria-valuemin', "0");
340+
expectAriaAttr(element, 'aria-valuemax', "100");
341+
342+
$rootScope.$apply('val=90');
343+
expectAriaAttr(element, 'aria-valuenow', "90");
344+
}));
345+
346+
it('should not attach if aria-value* is already present', inject(function($compile, $rootScope){
347+
var element = [
348+
$compile('<input type="range" ng-model="val" min="0" max="100" aria-valuenow="userSetValue1" aria-valuemin="userSetValue2" aria-valuemax="userSetValue3">')($rootScope),
349+
$compile('<div role="progressbar" min="0" max="100" ng-model="val" aria-valuenow="userSetValue1" aria-valuemin="userSetValue2" aria-valuemax="userSetValue3">')($rootScope),
350+
$compile('<div role="slider" min="0" max="100" ng-model="val" aria-valuenow="userSetValue1" aria-valuemin="userSetValue2" aria-valuemax="userSetValue3">')($rootScope)
351+
];
352+
353+
$rootScope.$apply('val=50');
354+
expectAriaAttr(element, 'aria-valuenow', "userSetValue1");
355+
expectAriaAttr(element, 'aria-valuemin', "userSetValue2");
356+
expectAriaAttr(element, 'aria-valuemax', "userSetValue3");
357+
}));
358+
359+
describe('disabled', function(){
360+
beforeEach(function(){
361+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
362+
$ariaProvider.config({
363+
ariaValue : false
364+
});
365+
});
366+
367+
module('ariaTest');
368+
});
369+
370+
it('should not attach itself when the option is disabled', inject(function($compile, $rootScope){
371+
var element = [
372+
$compile('<input type="range" ng-model="val" min="0" max="100">')($rootScope),
373+
$compile('<div role="progressbar" min="0" max="100" ng-model="val">')($rootScope)
374+
];
375+
376+
$rootScope.$apply('val=50');
377+
expectAriaAttr(element, 'aria-valuenow', undefined);
378+
expectAriaAttr(element, 'aria-valuemin', undefined);
379+
expectAriaAttr(element, 'aria-valuemax', undefined);
380+
}));
381+
});
382+
});
383+
384+
describe('tabindex', function(){
385+
beforeEach(module('ngAria'));
386+
387+
it('should attach tabindex to role=button, role=checkbox and ng-click', inject(function($compile, $rootScope){
388+
var element = [
389+
$compile("<div role='button'></div>")($rootScope),
390+
$compile("<div role='checkbox'></div>")($rootScope),
391+
$compile("<div ng-click='someAction()'></div>")($rootScope)
392+
];
393+
$rootScope.$digest();
394+
395+
expectAriaAttr(element, 'tabindex', '0');
396+
}));
397+
398+
it('should not attach tabindex to role=button, role=checkbox and ng-click if they are already present', inject(function($compile, $rootScope){
399+
var element = [
400+
$compile("<div role='button' tabindex='userSetValue'></div>")($rootScope),
401+
$compile("<div role='checkbox' tabindex='userSetValue'></div>")($rootScope),
402+
$compile("<div ng-click='someAction()' tabindex='userSetValue'></div>")($rootScope)
403+
];
404+
$rootScope.$digest();
405+
406+
expectAriaAttr(element, 'tabindex', 'userSetValue');
407+
}));
408+
409+
it('should set proper tabindex values for radiogroup', inject(function($compile, $rootScope){
410+
var element = $compile("<div role='radiogroup'><div role='radio' ng-model='val' value='one'>1</div><div role='radio' ng-model='val' value='two'>2</div></div>")($rootScope);
411+
412+
$rootScope.$apply("val='one'");
413+
expect(angular.element(angular.element(element).children()[0]).attr('tabindex')).toBe('0');
414+
expect(angular.element(angular.element(element).children()[1]).attr('tabindex')).toBe('-1');
415+
416+
$rootScope.$apply("val='two'");
417+
expect(angular.element(angular.element(element).children()[0]).attr('tabindex')).toBe('-1');
418+
expect(angular.element(angular.element(element).children()[1]).attr('tabindex')).toBe('0');
419+
420+
dealoc(element);
421+
}));
422+
423+
describe('disabled', function(){
424+
beforeEach(function(){
425+
angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){
426+
$ariaProvider.config({
427+
tabindex : false
428+
});
429+
});
430+
431+
module('ariaTest');
432+
});
433+
it('should not attach when disabled', inject(function($compile, $rootScope){
434+
var element = [
435+
$compile("<div role='button'></div>")($rootScope),
436+
$compile("<div role='checkbox'></div>")($rootScope),
437+
$compile("<div ng-click='someAction()'></div>")($rootScope)
438+
];
439+
$rootScope.$digest();
440+
expectAriaAttr(element, 'tabindex', undefined);
441+
}));
442+
});
443+
});
444+
});

0 commit comments

Comments
 (0)
Please sign in to comment.