diff --git a/src/ngAria/aria.js b/src/ngAria/aria.js
index 8c136676d4ad..57fff9fb07f3 100644
--- a/src/ngAria/aria.js
+++ b/src/ngAria/aria.js
@@ -110,10 +110,10 @@ function $AriaProvider() {
config = angular.extend(config, newConfig);
};
- function watchExpr(attrName, ariaAttr, negate) {
+ function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
return function(scope, elem, attr) {
var ariaCamelName = attr.$normalize(ariaAttr);
- if (config[ariaCamelName] && !attr[ariaCamelName]) {
+ if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
scope.$watch(attr[attrName], function(boolVal) {
if (negate) {
boolVal = !boolVal;
@@ -124,6 +124,12 @@ function $AriaProvider() {
};
}
+ function isNodeOneOf(elem, nodeTypeArray) {
+ if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
+ return true;
+ }
+ }
+
/**
* @ngdoc service
* @name $aria
@@ -175,22 +181,24 @@ function $AriaProvider() {
config: function(key) {
return config[key];
},
- $$watchExpr: watchExpr
+ $$watchExpr: watchExpr,
+ nodeBlackList: ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT'],
+ isNodeOneOf: isNodeOneOf
};
};
}
ngAriaModule.directive('ngShow', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngShow', 'aria-hidden', true);
+ return $aria.$$watchExpr('ngShow', 'aria-hidden', [], true);
}])
.directive('ngHide', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngHide', 'aria-hidden', false);
+ return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false);
}])
.directive('ngModel', ['$aria', function($aria) {
- function shouldAttachAttr(attr, normalizedAttr, elem) {
- return $aria.config(normalizedAttr) && !elem.attr(attr);
+ function shouldAttachAttr(attr, normalizedAttr, elem, nodeBlacklist) {
+ return !$aria.isNodeOneOf(elem, (nodeBlacklist || [])) && $aria.config(normalizedAttr) && !elem.attr(attr);
}
function shouldAttachRole(role, elem) {
@@ -203,6 +211,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' :
((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' :
+ (type || role) === 'radiogroup' ? 'radiogroup' :
(type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' :
(type || role) === 'textbox' || elem[0].nodeName === 'TEXTAREA' ? 'multiline' : '';
}
@@ -216,7 +225,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
return {
pre: function(scope, elem, attr, ngModel) {
- if (shape === 'checkbox' && attr.type !== 'checkbox') {
+ if (shape === 'checkbox') {
//Use the input[checkbox] $isEmpty implementation for elements with checkbox roles
ngModel.$isEmpty = function(value) {
return value === false;
@@ -224,7 +233,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
}
},
post: function(scope, elem, attr, ngModel) {
- var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem);
+ var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem, $aria.nodeBlackList);
function ngAriaWatchModelValue() {
return ngModel.$modelValue;
@@ -250,21 +259,29 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
}
switch (shape) {
+ case 'radiogroup':
+ if (needsTabIndex) {
+ elem.attr('tabindex', 0);
+ }
+ break;
case 'radio':
case 'checkbox':
if (shouldAttachRole(shape, elem)) {
elem.attr('role', shape);
}
- if (shouldAttachAttr('aria-checked', 'ariaChecked', elem)) {
+ if (shouldAttachAttr('aria-checked', 'ariaChecked', elem, ['INPUT'])) {
scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
getRadioReaction() : ngAriaCheckboxReaction);
}
+ if (needsTabIndex) {
+ elem.attr('tabindex', 0);
+ }
break;
case 'range':
if (shouldAttachRole(shape, elem)) {
elem.attr('role', 'slider');
}
- if ($aria.config('ariaValue')) {
+ if (!$aria.isNodeOneOf(elem, ['INPUT']) && $aria.config('ariaValue')) {
if (attr.min && !elem.attr('aria-valuemin')) {
elem.attr('aria-valuemin', attr.min);
}
@@ -277,6 +294,9 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
});
}
}
+ if (needsTabIndex) {
+ elem.attr('tabindex', 0);
+ }
break;
case 'multiline':
if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) {
@@ -285,11 +305,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
break;
}
- if (needsTabIndex) {
- elem.attr('tabindex', 0);
- }
-
- if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) {
+ if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem, $aria.nodeBlackList)) {
scope.$watch(function ngAriaRequiredWatch() {
return ngModel.$error.required;
}, function ngAriaRequiredReaction(newVal) {
@@ -310,7 +326,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
};
}])
.directive('ngDisabled', ['$aria', function($aria) {
- return $aria.$$watchExpr('ngDisabled', 'aria-disabled');
+ return $aria.$$watchExpr('ngDisabled', 'aria-disabled', $aria.nodeBlackList);
}])
.directive('ngMessages', function() {
return {
@@ -329,33 +345,29 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
compile: function(elem, attr) {
var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true);
return function(scope, elem, attr) {
-
- var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA'];
-
- function isNodeOneOf(elem, nodeTypeArray) {
- if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
- return true;
+
+ if (!$aria.isNodeOneOf(elem, $aria.nodeBlackList)) {
+
+ if (!elem.attr('role')) {
+ elem.attr('role', 'button');
+ }
+
+ if ($aria.config('tabindex') && !elem.attr('tabindex')) {
+ elem.attr('tabindex', 0);
}
- }
- if (!elem.attr('role') && !isNodeOneOf(elem, nodeBlackList)) {
- elem.attr('role', 'button');
- }
-
- if ($aria.config('tabindex') && !elem.attr('tabindex')) {
- elem.attr('tabindex', 0);
- }
- if ($aria.config('bindKeypress') && !attr.ngKeypress && !isNodeOneOf(elem, nodeBlackList)) {
- elem.on('keypress', function(event) {
- var keyCode = event.which || event.keyCode;
- if (keyCode === 32 || keyCode === 13) {
- scope.$apply(callback);
- }
+ if ($aria.config('bindKeypress') && !attr.ngKeypress) {
+ elem.on('keypress', function(event) {
+ var keyCode = event.which || event.keyCode;
+ if (keyCode === 32 || keyCode === 13) {
+ scope.$apply(callback);
+ }
- function callback() {
- fn(scope, { $event: event });
- }
- });
+ function callback() {
+ fn(scope, { $event: event });
+ }
+ });
+ }
}
};
}
diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js
index 8bfe301123e6..b2f2258f8b23 100644
--- a/test/ngAria/ariaSpec.js
+++ b/test/ngAria/ariaSpec.js
@@ -79,9 +79,15 @@ describe('$aria', function() {
describe('aria-checked', function() {
beforeEach(injectScopeAndCompiler);
- it('should attach itself to input type="checkbox"', function() {
+ it('should not attach itself to input type="checkbox"', function() {
compileElement('');
+ expect(element.attr('aria-checked')).toBeUndefined();
+ });
+
+ it('should attach itself to custom checkbox', function() {
+ compileElement('');
+
scope.$apply('val = true');
expect(element.attr('aria-checked')).toBe('true');
@@ -89,20 +95,18 @@ describe('$aria', function() {
expect(element.attr('aria-checked')).toBe('false');
});
- it('should handle checkbox with string model values using ng(True|False)Value', function() {
- var element = $compile(''
- )(scope);
+ xit('should handle custom checkbox with string model values using ng(True|False)Value', function() {
+
+ compileElement('');
- scope.$apply('val="yes"');
expect(element.eq(0).attr('aria-checked')).toBe('true');
scope.$apply('val="no"');
expect(element.eq(0).attr('aria-checked')).toBe('false');
});
- it('should handle checkbox with integer model values using ngTrueValue', function() {
- var element = $compile('')(scope);
+ xit('should handle custom checkbox with integer model values using ngTrueValue', function() {
+ compileElement('');
scope.$apply('val=0');
expect(element.eq(0).attr('aria-checked')).toBe('true');
@@ -111,22 +115,28 @@ describe('$aria', function() {
expect(element.eq(0).attr('aria-checked')).toBe('false');
});
- it('should attach itself to input type="radio"', function() {
- var element = $compile('' +
- '')(scope);
+ it('should not attach itself to input type="radio"', function() {
+ compileElement(''+
+ '');
+
+ scope.$apply("val='one'");
+ expect(element.eq(0).attr('aria-checked')).toBeUndefined();
+ expect(element.eq(1).attr('aria-checked')).toBeUndefined();
+ });
+
+ it('should attach itself to custom radio', function() {
+ compileElement('
');
scope.$apply("val='one'");
expect(element.eq(0).attr('aria-checked')).toBe('true');
- expect(element.eq(1).attr('aria-checked')).toBe('false');
scope.$apply("val='two'");
expect(element.eq(0).attr('aria-checked')).toBe('false');
- expect(element.eq(1).attr('aria-checked')).toBe('true');
});
it('should handle radios with integer model values', function() {
- var element = $compile('
' +
- '
')(scope);
+ compileElement('
' +
+ '
');
scope.$apply('val=0');
expect(element.eq(0).attr('aria-checked')).toBe('true');
@@ -138,8 +148,8 @@ describe('$aria', function() {
});
it('should handle radios with boolean model values using ngValue', function() {
- var element = $compile('
' +
- '
')(scope);
+ compileElement('
' +
+ '
');
scope.$apply(function() {
scope.valExp = true;
@@ -192,8 +202,6 @@ describe('$aria', function() {
it('should not attach itself if an aria-checked value is already present', function() {
var element = [
- $compile("
")(scope),
- $compile("
")(scope),
$compile("
")(scope),
$compile("
")(scope),
$compile("
")(scope),
@@ -224,7 +232,7 @@ describe('$aria', function() {
it('should not add a role to a native checkbox', function() {
compileElement('
');
- expect(element.attr('role')).toBe(undefined);
+ expect(element.attr('role')).toBeUndefined();
});
it('should add missing role="radio" to custom input', function() {
@@ -234,7 +242,7 @@ describe('$aria', function() {
it('should not add a role to a native radio button', function() {
compileElement('');
- expect(element.attr('role')).toBe(undefined);
+ expect(element.attr('role')).toBeUndefined();
});
it('should add missing role="slider" to custom input', function() {
@@ -244,7 +252,7 @@ describe('$aria', function() {
it('should not add a role to a native range input', function() {
compileElement('');
- expect(element.attr('role')).toBe(undefined);
+ expect(element.attr('role')).toBeUndefined();
});
});
@@ -272,52 +280,32 @@ describe('$aria', function() {
describe('aria-disabled', function() {
beforeEach(injectScopeAndCompiler);
- it('should attach itself to input elements', function() {
- scope.$apply('val = false');
- compileElement("");
- expect(element.attr('aria-disabled')).toBe('false');
-
- scope.$apply('val = true');
- expect(element.attr('aria-disabled')).toBe('true');
- });
-
- it('should attach itself to textarea elements', function() {
- scope.$apply('val = false');
- compileElement('');
- expect(element.attr('aria-disabled')).toBe('false');
-
- scope.$apply('val = true');
- expect(element.attr('aria-disabled')).toBe('true');
- });
-
- it('should attach itself to button elements', function() {
+ it('should attach itself to custom elements', function() {
+ compileElement('');
scope.$apply('val = false');
- compileElement('');
expect(element.attr('aria-disabled')).toBe('false');
scope.$apply('val = true');
expect(element.attr('aria-disabled')).toBe('true');
});
- it('should attach itself to select elements', function() {
- scope.$apply('val = false');
- compileElement('');
- expect(element.attr('aria-disabled')).toBe('false');
+ it('should not attach itself if an aria-disabled attribute is already present', function() {
+ compileElement('');
scope.$apply('val = true');
- expect(element.attr('aria-disabled')).toBe('true');
+ expect(element.attr('aria-disabled')).toBe('userSetValue');
});
- it('should not attach itself if an aria-disabled attribute is already present', function() {
+ it('should not attach itself to native controls', function() {
var element = [
- $compile("")(scope),
- $compile("")(scope),
- $compile("")(scope),
- $compile("")(scope)
+ $compile("")(scope),
+ $compile("")(scope),
+ $compile("")(scope),
+ $compile("")(scope)
];
scope.$apply('val = true');
- expectAriaAttrOnEachElement(element, 'aria-disabled', 'userSetValue');
+ expectAriaAttrOnEachElement(element, 'aria-disabled', undefined);
});
});
@@ -328,14 +316,9 @@ describe('$aria', function() {
beforeEach(injectScopeAndCompiler);
it('should not attach aria-disabled', function() {
- var element = [
- $compile("")(scope),
- $compile("")(scope),
- $compile("")(scope),
- $compile("")(scope)
- ];
+ compileElement('');
- scope.$apply('val = false');
+ scope.$apply('val = true');
expectAriaAttrOnEachElement(element, 'aria-disabled', undefined);
});
});
@@ -375,24 +358,17 @@ describe('$aria', function() {
describe('aria-required', function() {
beforeEach(injectScopeAndCompiler);
- it('should attach aria-required to input', function() {
- compileElement('');
- expect(element.attr('aria-required')).toBe('true');
-
- scope.$apply("val='input is valid now'");
- expect(element.attr('aria-required')).toBe('false');
- });
-
- it('should attach aria-required to textarea', function() {
- compileElement('');
- expect(element.attr('aria-required')).toBe('true');
-
- scope.$apply("val='input is valid now'");
- expect(element.attr('aria-required')).toBe('false');
+ it('should not attach aria-required to native controls', function() {
+ var element = [
+ $compile("")(scope),
+ $compile("")(scope),
+ $compile("")(scope)
+ ];
+ expectAriaAttrOnEachElement(element, 'aria-required', undefined);
});
- it('should attach aria-required to select', function() {
- compileElement('');
+ it('should attach aria-required to custom controls', function() {
+ compileElement('');
expect(element.attr('aria-required')).toBe('true');
scope.$apply("val='input is valid now'");
@@ -400,7 +376,7 @@ describe('$aria', function() {
});
it('should attach aria-required to ngRequired', function() {
- compileElement('');
+ compileElement('');
expect(element.attr('aria-required')).toBe('true');
scope.$apply("val='input is valid now'");
@@ -408,16 +384,7 @@ describe('$aria', function() {
});
it('should not attach itself if aria-required is already present', function() {
- compileElement("");
- expect(element.attr('aria-required')).toBe('userSetValue');
-
- compileElement("");
- expect(element.attr('aria-required')).toBe('userSetValue');
-
- compileElement("");
- expect(element.attr('aria-required')).toBe('userSetValue');
-
- compileElement("");
+ compileElement("");
expect(element.attr('aria-required')).toBe('userSetValue');
});
});
@@ -429,13 +396,7 @@ describe('$aria', function() {
beforeEach(injectScopeAndCompiler);
it('should not add the aria-required attribute', function() {
- compileElement("");
- expect(element.attr('aria-required')).toBeUndefined();
-
- compileElement("");
- expect(element.attr('aria-required')).toBeUndefined();
-
- compileElement("");
+ compileElement("");
expect(element.attr('aria-required')).toBeUndefined();
});
});
@@ -482,33 +443,34 @@ describe('$aria', function() {
describe('aria-value', function() {
beforeEach(injectScopeAndCompiler);
- it('should attach to input type="range"', function() {
+ it('should not attach to input type="range"', function() {
+ compileElement('');
+ scope.$apply('val = 50');
+ expect(element.attr('aria-valuenow')).toBeUndefined();
+ });
+
+ it('should attach to custom range controls', function() {
var element = [
- $compile('')(scope),
+ $compile('')(scope),
$compile('
')(scope),
$compile('
')(scope)
];
-
+
scope.$apply('val = 50');
- expectAriaAttrOnEachElement(element, 'aria-valuenow', "50");
- expectAriaAttrOnEachElement(element, 'aria-valuemin', "0");
- expectAriaAttrOnEachElement(element, 'aria-valuemax', "100");
+ expectAriaAttrOnEachElement(element, 'aria-valuenow', '50');
+ expectAriaAttrOnEachElement(element, 'aria-valuemin', '0');
+ expectAriaAttrOnEachElement(element, 'aria-valuemax', '100');
scope.$apply('val = 90');
- expectAriaAttrOnEachElement(element, 'aria-valuenow', "90");
+ expectAriaAttrOnEachElement(element, 'aria-valuenow', '90');
});
it('should not attach if aria-value* is already present', function() {
- var element = [
- $compile('
')(scope),
- $compile('
')(scope),
- $compile('
')(scope)
- ];
-
+ compileElement('
');
scope.$apply('val = 50');
- expectAriaAttrOnEachElement(element, 'aria-valuenow', 'userSetValue1');
- expectAriaAttrOnEachElement(element, 'aria-valuemin', 'userSetValue2');
- expectAriaAttrOnEachElement(element, 'aria-valuemax', 'userSetValue3');
+ expect(element.attr('aria-valuenow')).toBe('userSetValue1');
+ expect(element.attr('aria-valuemin')).toBe('userSetValue2');
+ expect(element.attr('aria-valuemax')).toBe('userSetValue3');
});
});
@@ -532,11 +494,6 @@ describe('$aria', function() {
it('should not attach itself', function() {
scope.$apply('val = 50');
- compileElement('
');
- expect(element.attr('aria-valuenow')).toBeUndefined();
- expect(element.attr('aria-valuemin')).toBeUndefined();
- expect(element.attr('aria-valuemax')).toBeUndefined();
-
compileElement('
');
expect(element.attr('aria-valuenow')).toBeUndefined();
expect(element.attr('aria-valuemin')).toBeUndefined();
@@ -547,10 +504,34 @@ describe('$aria', function() {
describe('tabindex', function() {
beforeEach(injectScopeAndCompiler);
- it('should attach tabindex to role="checkbox", ng-click, and ng-dblclick', function() {
+ it('should not attach to native controls', function() {
+ var element = [
+ $compile("
")(scope),
+ $compile("
")(scope),
+ $compile("")(scope),
+ $compile("")(scope),
+ $compile("")(scope)
+ ];
+ expectAriaAttrOnEachElement(element, 'tabindex', undefined);
+ });
+
+ it('should not attach to rando ng-model elements', function() {
+ compileElement('');
+ expect(element.attr('tabindex')).toBeUndefined();
+ });
+
+ it('should attach tabindex to custom inputs', function() {
+ compileElement('');
+ expect(element.attr('tabindex')).toBe('0');
+
compileElement('');
expect(element.attr('tabindex')).toBe('0');
+ compileElement('');
+ expect(element.attr('tabindex')).toBe('0');
+ });
+
+ it('should attach to ng-click and ng-dblclick', function() {
compileElement('');
expect(element.attr('tabindex')).toBe('0');
@@ -573,23 +554,18 @@ describe('$aria', function() {
});
it('should set proper tabindex values for radiogroup', function() {
- compileElement('' +
- '
1
' +
- '
2
' +
+ compileElement('
');
var one = element.contents().eq(0);
var two = element.contents().eq(1);
scope.$apply("val = 'one'");
- expect(one.attr('tabindex')).toBe('0');
- expect(two.attr('tabindex')).toBe('-1');
-
- scope.$apply("val = 'two'");
- expect(one.attr('tabindex')).toBe('-1');
- expect(two.attr('tabindex')).toBe('0');
-
- dealoc(element);
+ expect(element.eq(0).attr('tabindex')).toBe('0');
+ expect(one.attr('tabindex')).toBeUndefined();
+ expect(two.attr('tabindex')).toBeUndefined();
});
});