Skip to content

Commit 3c67f1d

Browse files
committed
feat(ngAria): add support for ignoring a specific element
Fixes angular#14602 Fixes angular#14672
1 parent b6f3c78 commit 3c67f1d

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
@@ -16,8 +16,8 @@
1616
*
1717
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
1818
* directives are supported:
19-
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
20-
* `ngDblClick`, and `ngMessages`.
19+
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`,
20+
* `ngClick`, `ngDblClick`, and `ngMessages`.
2121
*
2222
* Below is a more detailed breakdown of the attributes handled by ngAria:
2323
*
@@ -48,12 +48,18 @@
4848
* <md-checkbox ng-disabled="disabled" aria-disabled="true">
4949
* ```
5050
*
51-
* ## Disabling Attributes
52-
* It's possible to disable individual attributes added by ngAria with the
51+
* ## Disabling Specific Attributes
52+
* It is possible to disable individual attributes added by ngAria with the
5353
* {@link ngAria.$ariaProvider#config config} method. For more details, see the
5454
* {@link guide/accessibility Developer Guide}.
55+
*
56+
* ## Disabling `ngAria` on Specific Elements
57+
* It is possible to make `ngAria` ignore a specific element, by adding the `ng-aria-disable`
58+
* attribute on it. Note that only the element itself (and not its child elements) will be ignored.
5559
*/
5660
/* global -ngAriaModule */
61+
var ARIA_DISABLE_ATTR = 'ngAriaDisable';
62+
5763
var ngAriaModule = angular.module('ngAria', ['ng']).
5864
provider('$aria', $AriaProvider);
5965

@@ -130,6 +136,8 @@ function $AriaProvider() {
130136

131137
function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
132138
return function(scope, elem, attr) {
139+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
140+
133141
var ariaCamelName = attr.$normalize(ariaAttr);
134142
if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
135143
scope.$watch(attr[attrName], function(boolVal) {
@@ -246,6 +254,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
246254
require: 'ngModel',
247255
priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
248256
compile: function(elem, attr) {
257+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
258+
249259
var shape = getShape(attr, elem);
250260

251261
return {
@@ -350,6 +360,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
350360
restrict: 'A',
351361
require: '?ngMessages',
352362
link: function(scope, elem, attr, ngMessages) {
363+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
364+
353365
if (!elem.attr('aria-live')) {
354366
elem.attr('aria-live', 'assertive');
355367
}
@@ -360,6 +372,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
360372
return {
361373
restrict: 'A',
362374
compile: function(elem, attr) {
375+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
376+
363377
var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true);
364378
return function(scope, elem, attr) {
365379

@@ -392,6 +406,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
392406
}])
393407
.directive('ngDblclick', ['$aria', function($aria) {
394408
return function(scope, elem, attr) {
409+
if (attr.hasOwnProperty(ARIA_DISABLE_ATTR)) return;
410+
395411
if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) {
396412
elem.attr('tabindex', 0);
397413
}

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)