feat(ngAria): add support for ignoring a specific element

Fixes #14602
Fixes #14672

Closes #14833
This commit is contained in:
Georgios Kalpakas
2016-06-28 00:50:54 +03:00
committed by George Kalpakas
parent 83d1229c87
commit db584f7835
2 changed files with 251 additions and 4 deletions
+20 -4
View File
@@ -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 @@
* <md-checkbox ng-disabled="disabled" aria-disabled="true">
* ```
*
* ## 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);
}
+231
View File
@@ -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('<div role="checkbox" ng-model="val" ng-aria-disable></div>');
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(
'<div role="radio" ng-model="val" value="one" ng-aria-disable></div>' +
'<div role="radio" ng-model="val" value="two" ng-aria-disable></div>');
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('<div ng-disabled="val" ng-aria-disable></div>');
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('<div ng-show="val" ng-aria-disable></div>');
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('<div ng-hide="val" ng-aria-disable></div>');
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('<input ng-model="val" ng-minlength="10" ng-aria-disable />');
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('<div role="textbox" ng-model="val" ng-minlength="10" ng-aria-disable></div>');
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('<div ng-messages="val" ng-aria-disable>');
expect(element).not.toHaveAttribute('aria-live');
});
// ariaReadonly
it('should not attach aria-readonly to custom controls', function() {
compileElement('<div ng-readonly="val" ng-aria-disable></div>');
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('<div ng-model="val" required ng-aria-disable></div>');
expect(element).not.toHaveAttribute('aria-required');
});
it('should not attach aria-required to custom controls with `ngRequired`', function() {
compileElement('<div ng-model="val" ng-required="val" ng-aria-disable></div>');
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('<input type="range" ng-model="val" min="0" max="100" ng-aria-disable />');
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(
'<div role="progressbar" ng-model="val" min="0" max="100" ng-aria-disable></div>' +
'<div role="slider" ng-model="val" min="0" max="100" ng-aria-disable></div>');
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(
'<div ng-click="onClick()" tabindex="0" ng-aria-disable></div>' +
'<ul><li ng-click="onClick()" tabindex="0" ng-aria-disable></li></ul>');
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(
'<div ng-click="onClick()" ng-aria-disable></div>' +
'<div type="checkbox" ng-model="val" ng-aria-disable></div>' +
'<div type="radio" ng-model="val" ng-aria-disable></div>' +
'<div type="range" ng-model="val" ng-aria-disable></div>');
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(
'<div role="checkbox" ng-model="val" ng-aria-disable></div>' +
'<div role="slider" ng-model="val" ng-aria-disable></div>');
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(
'<div ng-click="onClick()" ng-aria-disable></div>' +
'<div ng-dblclick="onDblclick()" ng-aria-disable></div>');
expect(element.eq(0)).not.toHaveAttribute('tabindex');
expect(element.eq(1)).not.toHaveAttribute('tabindex');
});
});
describe('aria-hidden', function() {
beforeEach(injectScopeAndCompiler);