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('
' + + '
1
' + + '
2
' + '
'); 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(); }); });