From 15a97a5b8aca66f9c2b7768ccdc0704e4cac40fa Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Sun, 26 Jun 2016 22:24:57 +0300 Subject: [PATCH 1/2] refactor(ngAria): move test helpers inside of closure --- test/ngAria/ariaSpec.js | 50 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js index 201608498961..58cd6584157f 100644 --- a/test/ngAria/ariaSpec.js +++ b/test/ngAria/ariaSpec.js @@ -9,18 +9,6 @@ describe('$aria', function() { dealoc(element); }); - function injectScopeAndCompiler() { - return inject(function(_$compile_, _$rootScope_) { - $compile = _$compile_; - scope = _$rootScope_; - }); - } - - function compileElement(inputHtml) { - element = $compile(inputHtml)(scope); - scope.$digest(); - } - describe('aria-hidden', function() { beforeEach(injectScopeAndCompiler); @@ -895,19 +883,31 @@ describe('$aria', function() { expect(element.attr('tabindex')).toBe('0'); }); }); -}); -function expectAriaAttrOnEachElement(elem, ariaAttr, expected) { - angular.forEach(elem, function(val) { - expect(angular.element(val).attr(ariaAttr)).toBe(expected); - }); -} + // Helpers + function compileElement(inputHtml) { + element = $compile(inputHtml)(scope); + scope.$digest(); + } + + function configAriaProvider(config) { + return function() { + module(function($ariaProvider) { + $ariaProvider.config(config); + }); + }; + } -function configAriaProvider(config) { - return function() { - angular.module('ariaTest', ['ngAria']).config(function($ariaProvider) { - $ariaProvider.config(config); + function expectAriaAttrOnEachElement(elem, ariaAttr, expected) { + angular.forEach(elem, function(val) { + expect(angular.element(val).attr(ariaAttr)).toBe(expected); }); - module('ariaTest'); - }; -} + } + + function injectScopeAndCompiler() { + return inject(function(_$compile_, _$rootScope_) { + $compile = _$compile_; + scope = _$rootScope_; + }); + } +}); From 4a7a43872dfc347c6eb24f1c0f575ad30dacdca3 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Tue, 28 Jun 2016 00:50:54 +0300 Subject: [PATCH 2/2] feat(ngAria): add support for ignoring a specific element Fixes #14602 Fixes #14672 --- src/ngAria/aria.js | 24 ++++- test/ngAria/ariaSpec.js | 231 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+), 4 deletions(-) diff --git a/src/ngAria/aria.js b/src/ngAria/aria.js index bf82141b2123..904d9a0e5959 100644 --- a/src/ngAria/aria.js +++ b/src/ngAria/aria.js @@ -14,8 +14,8 @@ * * For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following * directives are supported: - * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`, - * `ngDblClick`, and `ngMessages`. + * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, + * `ngClick`, `ngDblClick`, and `ngMessages`. * * Below is a more detailed breakdown of the attributes handled by ngAria: * @@ -46,11 +46,17 @@ * * ``` * - * ## Disabling Attributes - * It's possible to disable individual attributes added by ngAria with the + * ## Disabling Specific Attributes + * It is possible to disable individual attributes added by ngAria with the * {@link ngAria.$ariaProvider#config config} method. For more details, see the * {@link guide/accessibility Developer Guide}. + * + * ## Disabling `ngAria` on Specific Elements + * It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable` + * attribute on it. Note that only the element itself (and not its child elements) will be ignored. */ +var ARIA_DISABLE_ATTR = 'ngAriaDisable'; + var ngAriaModule = angular.module('ngAria', ['ng']). info({ angularVersion: '"NG_VERSION_FULL"' }). provider('$aria', $AriaProvider); @@ -132,6 +138,8 @@ function $AriaProvider() { function watchExpr(attrName, ariaAttr, nodeBlackList, negate) { return function(scope, elem, attr) { + if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return; + var ariaCamelName = attr.$normalize(ariaAttr); if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) { scope.$watch(attr[attrName], function(boolVal) { @@ -251,6 +259,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { require: 'ngModel', priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value compile: function(elem, attr) { + if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return; + var shape = getShape(attr, elem); return { @@ -347,6 +357,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { restrict: 'A', require: '?ngMessages', link: function(scope, elem, attr, ngMessages) { + if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return; + if (!elem.attr('aria-live')) { elem.attr('aria-live', 'assertive'); } @@ -357,6 +369,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { return { restrict: 'A', compile: function(elem, attr) { + if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return; + var fn = $parse(attr.ngClick); return function(scope, elem, attr) { @@ -389,6 +403,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { }]) .directive('ngDblclick', ['$aria', function($aria) { return function(scope, elem, attr) { + if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return; + if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) { elem.attr('tabindex', 0); } diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js index 58cd6584157f..1970b01438b0 100644 --- a/test/ngAria/ariaSpec.js +++ b/test/ngAria/ariaSpec.js @@ -9,6 +9,237 @@ describe('$aria', function() { dealoc(element); }); + describe('with `ngAriaDisable`', function() { + beforeEach(injectScopeAndCompiler); + beforeEach(function() { + jasmine.addMatchers({ + toHaveAttribute: function toHaveAttributeMatcher() { + return { + compare: function toHaveAttributeCompare(element, attr) { + var node = element[0]; + var pass = node.hasAttribute(attr); + var message = 'Expected `' + node.outerHTML + '` ' + (pass ? 'not ' : '') + + 'to have attribute `' + attr + '`.'; + + return { + pass: pass, + message: message + }; + } + }; + } + }); + }); + + // ariaChecked + it('should not attach aria-checked to custom checkbox', function() { + compileElement('
'); + + scope.$apply('val = false'); + expect(element).not.toHaveAttribute('aria-checked'); + + scope.$apply('val = true'); + expect(element).not.toHaveAttribute('aria-checked'); + }); + + it('should not attach aria-checked to custom radio controls', function() { + compileElement( + '
' + + '
'); + + var radio1 = element.eq(0); + var radio2 = element.eq(1); + + scope.$apply('val = "one"'); + expect(radio1).not.toHaveAttribute('aria-checked'); + expect(radio2).not.toHaveAttribute('aria-checked'); + + scope.$apply('val = "two"'); + expect(radio1).not.toHaveAttribute('aria-checked'); + expect(radio2).not.toHaveAttribute('aria-checked'); + }); + + // ariaDisabled + it('should not attach aria-disabled to custom controls', function() { + compileElement('
'); + + scope.$apply('val = false'); + expect(element).not.toHaveAttribute('aria-disabled'); + + scope.$apply('val = true'); + expect(element).not.toHaveAttribute('aria-disabled'); + }); + + // ariaHidden + it('should not attach aria-hidden to `ngShow`', function() { + compileElement('
'); + + scope.$apply('val = false'); + expect(element).not.toHaveAttribute('aria-hidden'); + + scope.$apply('val = true'); + expect(element).not.toHaveAttribute('aria-hidden'); + }); + + it('should not attach aria-hidden to `ngHide`', function() { + compileElement('
'); + + scope.$apply('val = false'); + expect(element).not.toHaveAttribute('aria-hidden'); + + scope.$apply('val = true'); + expect(element).not.toHaveAttribute('aria-hidden'); + }); + + // ariaInvalid + it('should not attach aria-invalid to input', function() { + compileElement(''); + + scope.$apply('val = "lt 10"'); + expect(element).not.toHaveAttribute('aria-invalid'); + + scope.$apply('val = "gt 10 characters"'); + expect(element).not.toHaveAttribute('aria-invalid'); + }); + + it('should not attach aria-invalid to custom controls', function() { + compileElement('
'); + + scope.$apply('val = "lt 10"'); + expect(element).not.toHaveAttribute('aria-invalid'); + + scope.$apply('val = "gt 10 characters"'); + expect(element).not.toHaveAttribute('aria-invalid'); + }); + + // ariaLive + it('should not attach aria-live to `ngMessages`', function() { + compileElement('
'); + expect(element).not.toHaveAttribute('aria-live'); + }); + + // ariaReadonly + it('should not attach aria-readonly to custom controls', function() { + compileElement('
'); + + scope.$apply('val = false'); + expect(element).not.toHaveAttribute('aria-readonly'); + + scope.$apply('val = true'); + expect(element).not.toHaveAttribute('aria-readonly'); + }); + + // ariaRequired + it('should not attach aria-required to custom controls with `required`', function() { + compileElement('
'); + expect(element).not.toHaveAttribute('aria-required'); + }); + + it('should not attach aria-required to custom controls with `ngRequired`', function() { + compileElement('
'); + + scope.$apply('val = false'); + expect(element).not.toHaveAttribute('aria-required'); + + scope.$apply('val = true'); + expect(element).not.toHaveAttribute('aria-required'); + }); + + // ariaValue + it('should not attach aria-value* to input[range]', function() { + compileElement(''); + + expect(element).not.toHaveAttribute('aria-valuemax'); + expect(element).not.toHaveAttribute('aria-valuemin'); + expect(element).not.toHaveAttribute('aria-valuenow'); + + scope.$apply('val = 50'); + expect(element).not.toHaveAttribute('aria-valuemax'); + expect(element).not.toHaveAttribute('aria-valuemin'); + expect(element).not.toHaveAttribute('aria-valuenow'); + + scope.$apply('val = 150'); + expect(element).not.toHaveAttribute('aria-valuemax'); + expect(element).not.toHaveAttribute('aria-valuemin'); + expect(element).not.toHaveAttribute('aria-valuenow'); + }); + + it('should not attach aria-value* to custom controls', function() { + compileElement( + '
' + + '
'); + + var progressbar = element.eq(0); + var slider = element.eq(1); + + ['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) { + expect(progressbar).not.toHaveAttribute(attr); + expect(slider).not.toHaveAttribute(attr); + }); + + scope.$apply('val = 50'); + ['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) { + expect(progressbar).not.toHaveAttribute(attr); + expect(slider).not.toHaveAttribute(attr); + }); + + scope.$apply('val = 150'); + ['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) { + expect(progressbar).not.toHaveAttribute(attr); + expect(slider).not.toHaveAttribute(attr); + }); + }); + + // bindKeypress + it('should not bind keypress to `ngClick`', function() { + scope.onClick = jasmine.createSpy('onClick'); + compileElement( + '
' + + '
'); + + var div = element.find('div'); + var li = element.find('li'); + + div.triggerHandler({type: 'keypress', keyCode: 32}); + li.triggerHandler({type: 'keypress', keyCode: 32}); + + expect(scope.onClick).not.toHaveBeenCalled(); + }); + + // bindRoleForClick + it('should not attach role to custom controls', function() { + compileElement( + '
' + + '
' + + '
' + + '
'); + + expect(element.eq(0)).not.toHaveAttribute('role'); + expect(element.eq(1)).not.toHaveAttribute('role'); + expect(element.eq(2)).not.toHaveAttribute('role'); + expect(element.eq(3)).not.toHaveAttribute('role'); + }); + + // tabindex + it('should not attach tabindex to custom controls', function() { + compileElement( + '
' + + '
'); + + expect(element.eq(0)).not.toHaveAttribute('tabindex'); + expect(element.eq(1)).not.toHaveAttribute('tabindex'); + }); + + it('should not attach tabindex to `ngClick` or `ngDblclick`', function() { + compileElement( + '
' + + '
'); + + expect(element.eq(0)).not.toHaveAttribute('tabindex'); + expect(element.eq(1)).not.toHaveAttribute('tabindex'); + }); + }); + describe('aria-hidden', function() { beforeEach(injectScopeAndCompiler);