Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 4a7a438

Browse files
committed
feat(ngAria): add support for ignoring a specific element
Fixes #14602 Fixes #14672
1 parent 15a97a5 commit 4a7a438

File tree

2 files changed

+251
-4
lines changed

2 files changed

+251
-4
lines changed

src/ngAria/aria.js

+20-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
*
1515
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
1616
* directives are supported:
17-
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
18-
* `ngDblClick`, and `ngMessages`.
17+
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`,
18+
* `ngClick`, `ngDblClick`, and `ngMessages`.
1919
*
2020
* Below is a more detailed breakdown of the attributes handled by ngAria:
2121
*
@@ -46,11 +46,17 @@
4646
* <md-checkbox ng-disabled="disabled" aria-disabled="true">
4747
* ```
4848
*
49-
* ## Disabling Attributes
50-
* It's possible to disable individual attributes added by ngAria with the
49+
* ## Disabling Specific Attributes
50+
* It is possible to disable individual attributes added by ngAria with the
5151
* {@link ngAria.$ariaProvider#config config} method. For more details, see the
5252
* {@link guide/accessibility Developer Guide}.
53+
*
54+
* ## Disabling `ngAria` on Specific Elements
55+
* It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable`
56+
* attribute on it. Note that only the element itself (and not its child elements) will be ignored.
5357
*/
58+
var ARIA_DISABLE_ATTR = 'ngAriaDisable';
59+
5460
var ngAriaModule = angular.module('ngAria', ['ng']).
5561
info({ angularVersion: '"NG_VERSION_FULL"' }).
5662
provider('$aria', $AriaProvider);
@@ -132,6 +138,8 @@ function $AriaProvider() {
132138

133139
function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
134140
return function(scope, elem, attr) {
141+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
142+
135143
var ariaCamelName = attr.$normalize(ariaAttr);
136144
if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
137145
scope.$watch(attr[attrName], function(boolVal) {
@@ -251,6 +259,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
251259
require: 'ngModel',
252260
priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
253261
compile: function(elem, attr) {
262+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
263+
254264
var shape = getShape(attr, elem);
255265

256266
return {
@@ -347,6 +357,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
347357
restrict: 'A',
348358
require: '?ngMessages',
349359
link: function(scope, elem, attr, ngMessages) {
360+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
361+
350362
if (!elem.attr('aria-live')) {
351363
elem.attr('aria-live', 'assertive');
352364
}
@@ -357,6 +369,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
357369
return {
358370
restrict: 'A',
359371
compile: function(elem, attr) {
372+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
373+
360374
var fn = $parse(attr.ngClick);
361375
return function(scope, elem, attr) {
362376

@@ -389,6 +403,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
389403
}])
390404
.directive('ngDblclick', ['$aria', function($aria) {
391405
return function(scope, elem, attr) {
406+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
407+
392408
if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) {
393409
elem.attr('tabindex', 0);
394410
}

test/ngAria/ariaSpec.js

+231
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,237 @@ describe('$aria', function() {
99
dealoc(element);
1010
});
1111

12+
describe('with `ngAriaDisable`', function() {
13+
beforeEach(injectScopeAndCompiler);
14+
beforeEach(function() {
15+
jasmine.addMatchers({
16+
toHaveAttribute: function toHaveAttributeMatcher() {
17+
return {
18+
compare: function toHaveAttributeCompare(element, attr) {
19+
var node = element[0];
20+
var pass = node.hasAttribute(attr);
21+
var message = 'Expected `' + node.outerHTML + '` ' + (pass ? 'not ' : '') +
22+
'to have attribute `' + attr + '`.';
23+
24+
return {
25+
pass: pass,
26+
message: message
27+
};
28+
}
29+
};
30+
}
31+
});
32+
});
33+
34+
// ariaChecked
35+
it('should not attach aria-checked to custom checkbox', function() {
36+
compileElement('<div role="checkbox" ng-model="val" ng-aria-disable></div>');
37+
38+
scope.$apply('val = false');
39+
expect(element).not.toHaveAttribute('aria-checked');
40+
41+
scope.$apply('val = true');
42+
expect(element).not.toHaveAttribute('aria-checked');
43+
});
44+
45+
it('should not attach aria-checked to custom radio controls', function() {
46+
compileElement(
47+
'<div role="radio" ng-model="val" value="one" ng-aria-disable></div>' +
48+
'<div role="radio" ng-model="val" value="two" ng-aria-disable></div>');
49+
50+
var radio1 = element.eq(0);
51+
var radio2 = element.eq(1);
52+
53+
scope.$apply('val = "one"');
54+
expect(radio1).not.toHaveAttribute('aria-checked');
55+
expect(radio2).not.toHaveAttribute('aria-checked');
56+
57+
scope.$apply('val = "two"');
58+
expect(radio1).not.toHaveAttribute('aria-checked');
59+
expect(radio2).not.toHaveAttribute('aria-checked');
60+
});
61+
62+
// ariaDisabled
63+
it('should not attach aria-disabled to custom controls', function() {
64+
compileElement('<div ng-disabled="val" ng-aria-disable></div>');
65+
66+
scope.$apply('val = false');
67+
expect(element).not.toHaveAttribute('aria-disabled');
68+
69+
scope.$apply('val = true');
70+
expect(element).not.toHaveAttribute('aria-disabled');
71+
});
72+
73+
// ariaHidden
74+
it('should not attach aria-hidden to `ngShow`', function() {
75+
compileElement('<div ng-show="val" ng-aria-disable></div>');
76+
77+
scope.$apply('val = false');
78+
expect(element).not.toHaveAttribute('aria-hidden');
79+
80+
scope.$apply('val = true');
81+
expect(element).not.toHaveAttribute('aria-hidden');
82+
});
83+
84+
it('should not attach aria-hidden to `ngHide`', function() {
85+
compileElement('<div ng-hide="val" ng-aria-disable></div>');
86+
87+
scope.$apply('val = false');
88+
expect(element).not.toHaveAttribute('aria-hidden');
89+
90+
scope.$apply('val = true');
91+
expect(element).not.toHaveAttribute('aria-hidden');
92+
});
93+
94+
// ariaInvalid
95+
it('should not attach aria-invalid to input', function() {
96+
compileElement('<input ng-model="val" ng-minlength="10" ng-aria-disable />');
97+
98+
scope.$apply('val = "lt 10"');
99+
expect(element).not.toHaveAttribute('aria-invalid');
100+
101+
scope.$apply('val = "gt 10 characters"');
102+
expect(element).not.toHaveAttribute('aria-invalid');
103+
});
104+
105+
it('should not attach aria-invalid to custom controls', function() {
106+
compileElement('<div role="textbox" ng-model="val" ng-minlength="10" ng-aria-disable></div>');
107+
108+
scope.$apply('val = "lt 10"');
109+
expect(element).not.toHaveAttribute('aria-invalid');
110+
111+
scope.$apply('val = "gt 10 characters"');
112+
expect(element).not.toHaveAttribute('aria-invalid');
113+
});
114+
115+
// ariaLive
116+
it('should not attach aria-live to `ngMessages`', function() {
117+
compileElement('<div ng-messages="val" ng-aria-disable>');
118+
expect(element).not.toHaveAttribute('aria-live');
119+
});
120+
121+
// ariaReadonly
122+
it('should not attach aria-readonly to custom controls', function() {
123+
compileElement('<div ng-readonly="val" ng-aria-disable></div>');
124+
125+
scope.$apply('val = false');
126+
expect(element).not.toHaveAttribute('aria-readonly');
127+
128+
scope.$apply('val = true');
129+
expect(element).not.toHaveAttribute('aria-readonly');
130+
});
131+
132+
// ariaRequired
133+
it('should not attach aria-required to custom controls with `required`', function() {
134+
compileElement('<div ng-model="val" required ng-aria-disable></div>');
135+
expect(element).not.toHaveAttribute('aria-required');
136+
});
137+
138+
it('should not attach aria-required to custom controls with `ngRequired`', function() {
139+
compileElement('<div ng-model="val" ng-required="val" ng-aria-disable></div>');
140+
141+
scope.$apply('val = false');
142+
expect(element).not.toHaveAttribute('aria-required');
143+
144+
scope.$apply('val = true');
145+
expect(element).not.toHaveAttribute('aria-required');
146+
});
147+
148+
// ariaValue
149+
it('should not attach aria-value* to input[range]', function() {
150+
compileElement('<input type="range" ng-model="val" min="0" max="100" ng-aria-disable />');
151+
152+
expect(element).not.toHaveAttribute('aria-valuemax');
153+
expect(element).not.toHaveAttribute('aria-valuemin');
154+
expect(element).not.toHaveAttribute('aria-valuenow');
155+
156+
scope.$apply('val = 50');
157+
expect(element).not.toHaveAttribute('aria-valuemax');
158+
expect(element).not.toHaveAttribute('aria-valuemin');
159+
expect(element).not.toHaveAttribute('aria-valuenow');
160+
161+
scope.$apply('val = 150');
162+
expect(element).not.toHaveAttribute('aria-valuemax');
163+
expect(element).not.toHaveAttribute('aria-valuemin');
164+
expect(element).not.toHaveAttribute('aria-valuenow');
165+
});
166+
167+
it('should not attach aria-value* to custom controls', function() {
168+
compileElement(
169+
'<div role="progressbar" ng-model="val" min="0" max="100" ng-aria-disable></div>' +
170+
'<div role="slider" ng-model="val" min="0" max="100" ng-aria-disable></div>');
171+
172+
var progressbar = element.eq(0);
173+
var slider = element.eq(1);
174+
175+
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
176+
expect(progressbar).not.toHaveAttribute(attr);
177+
expect(slider).not.toHaveAttribute(attr);
178+
});
179+
180+
scope.$apply('val = 50');
181+
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
182+
expect(progressbar).not.toHaveAttribute(attr);
183+
expect(slider).not.toHaveAttribute(attr);
184+
});
185+
186+
scope.$apply('val = 150');
187+
['aria-valuemax', 'aria-valuemin', 'aria-valuenow'].forEach(function(attr) {
188+
expect(progressbar).not.toHaveAttribute(attr);
189+
expect(slider).not.toHaveAttribute(attr);
190+
});
191+
});
192+
193+
// bindKeypress
194+
it('should not bind keypress to `ngClick`', function() {
195+
scope.onClick = jasmine.createSpy('onClick');
196+
compileElement(
197+
'<div ng-click="onClick()" tabindex="0" ng-aria-disable></div>' +
198+
'<ul><li ng-click="onClick()" tabindex="0" ng-aria-disable></li></ul>');
199+
200+
var div = element.find('div');
201+
var li = element.find('li');
202+
203+
div.triggerHandler({type: 'keypress', keyCode: 32});
204+
li.triggerHandler({type: 'keypress', keyCode: 32});
205+
206+
expect(scope.onClick).not.toHaveBeenCalled();
207+
});
208+
209+
// bindRoleForClick
210+
it('should not attach role to custom controls', function() {
211+
compileElement(
212+
'<div ng-click="onClick()" ng-aria-disable></div>' +
213+
'<div type="checkbox" ng-model="val" ng-aria-disable></div>' +
214+
'<div type="radio" ng-model="val" ng-aria-disable></div>' +
215+
'<div type="range" ng-model="val" ng-aria-disable></div>');
216+
217+
expect(element.eq(0)).not.toHaveAttribute('role');
218+
expect(element.eq(1)).not.toHaveAttribute('role');
219+
expect(element.eq(2)).not.toHaveAttribute('role');
220+
expect(element.eq(3)).not.toHaveAttribute('role');
221+
});
222+
223+
// tabindex
224+
it('should not attach tabindex to custom controls', function() {
225+
compileElement(
226+
'<div role="checkbox" ng-model="val" ng-aria-disable></div>' +
227+
'<div role="slider" ng-model="val" ng-aria-disable></div>');
228+
229+
expect(element.eq(0)).not.toHaveAttribute('tabindex');
230+
expect(element.eq(1)).not.toHaveAttribute('tabindex');
231+
});
232+
233+
it('should not attach tabindex to `ngClick` or `ngDblclick`', function() {
234+
compileElement(
235+
'<div ng-click="onClick()" ng-aria-disable></div>' +
236+
'<div ng-dblclick="onDblclick()" ng-aria-disable></div>');
237+
238+
expect(element.eq(0)).not.toHaveAttribute('tabindex');
239+
expect(element.eq(1)).not.toHaveAttribute('tabindex');
240+
});
241+
});
242+
12243
describe('aria-hidden', function() {
13244
beforeEach(injectScopeAndCompiler);
14245

0 commit comments

Comments
 (0)