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

Commit f48244c

Browse files
marcysuttongkalpak
authored andcommitted
fix(ngAria): clean up tabindex usage
* Do not put tabindex on native controls using ng-model or ng-click * Uses a single nodeBlacklist to limit which elements receive support Closes #11500
1 parent ebba426 commit f48244c

File tree

2 files changed

+65
-56
lines changed

2 files changed

+65
-56
lines changed

src/ngAria/aria.js

+39-35
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
var ngAriaModule = angular.module('ngAria', ['ng']).
5353
provider('$aria', $AriaProvider);
5454

55+
/**
56+
* Internal Utilities
57+
*/
58+
var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY'];
59+
60+
var isNodeOneOf = function (elem, nodeTypeArray) {
61+
if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
62+
return true;
63+
}
64+
}
5565
/**
5666
* @ngdoc provider
5767
* @name $ariaProvider
@@ -113,10 +123,10 @@ function $AriaProvider() {
113123
config = angular.extend(config, newConfig);
114124
};
115125

116-
function watchExpr(attrName, ariaAttr, negate) {
126+
function watchExpr(attrName, ariaAttr, nodeBlackList, negate) {
117127
return function(scope, elem, attr) {
118128
var ariaCamelName = attr.$normalize(ariaAttr);
119-
if (config[ariaCamelName] && !attr[ariaCamelName]) {
129+
if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) {
120130
scope.$watch(attr[attrName], function(boolVal) {
121131
// ensure boolean value
122132
boolVal = negate ? !boolVal : !!boolVal;
@@ -125,7 +135,6 @@ function $AriaProvider() {
125135
}
126136
};
127137
}
128-
129138
/**
130139
* @ngdoc service
131140
* @name $aria
@@ -184,10 +193,10 @@ function $AriaProvider() {
184193

185194

186195
ngAriaModule.directive('ngShow', ['$aria', function($aria) {
187-
return $aria.$$watchExpr('ngShow', 'aria-hidden', true);
196+
return $aria.$$watchExpr('ngShow', 'aria-hidden', [], true);
188197
}])
189198
.directive('ngHide', ['$aria', function($aria) {
190-
return $aria.$$watchExpr('ngHide', 'aria-hidden', false);
199+
return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false);
191200
}])
192201
.directive('ngModel', ['$aria', function($aria) {
193202

@@ -261,6 +270,9 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
261270
scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
262271
getRadioReaction() : ngAriaCheckboxReaction);
263272
}
273+
if (needsTabIndex) {
274+
elem.attr('tabindex', 0);
275+
}
264276
break;
265277
case 'range':
266278
if (shouldAttachRole(shape, elem)) {
@@ -289,6 +301,9 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
289301
});
290302
}
291303
}
304+
if (needsTabIndex) {
305+
elem.attr('tabindex', 0);
306+
}
292307
break;
293308
case 'multiline':
294309
if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) {
@@ -297,10 +312,6 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
297312
break;
298313
}
299314

300-
if (needsTabIndex) {
301-
elem.attr('tabindex', 0);
302-
}
303-
304315
if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) {
305316
scope.$watch(function ngAriaRequiredWatch() {
306317
return ngModel.$error.required;
@@ -322,7 +333,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
322333
};
323334
}])
324335
.directive('ngDisabled', ['$aria', function($aria) {
325-
return $aria.$$watchExpr('ngDisabled', 'aria-disabled');
336+
return $aria.$$watchExpr('ngDisabled', 'aria-disabled', []);
326337
}])
327338
.directive('ngMessages', function() {
328339
return {
@@ -342,43 +353,36 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
342353
var fn = $parse(attr.ngClick, /* interceptorFn */ null, /* expensiveChecks */ true);
343354
return function(scope, elem, attr) {
344355

345-
var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA'];
356+
if (!isNodeOneOf(elem, nodeBlackList)) {
346357

347-
function isNodeOneOf(elem, nodeTypeArray) {
348-
if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) {
349-
return true;
358+
if ($aria.config('bindRoleForClick') && !elem.attr('role')) {
359+
elem.attr('role', 'button');
350360
}
351-
}
352361

353-
if ($aria.config('bindRoleForClick')
354-
&& !elem.attr('role')
355-
&& !isNodeOneOf(elem, nodeBlackList)) {
356-
elem.attr('role', 'button');
357-
}
358-
359-
if ($aria.config('tabindex') && !elem.attr('tabindex')) {
360-
elem.attr('tabindex', 0);
361-
}
362+
if ($aria.config('tabindex') && !elem.attr('tabindex')) {
363+
elem.attr('tabindex', 0);
364+
}
362365

363-
if ($aria.config('bindKeypress') && !attr.ngKeypress && !isNodeOneOf(elem, nodeBlackList)) {
364-
elem.on('keypress', function(event) {
365-
var keyCode = event.which || event.keyCode;
366-
if (keyCode === 32 || keyCode === 13) {
367-
scope.$apply(callback);
368-
}
366+
if ($aria.config('bindKeypress') && !attr.ngKeypress) {
367+
elem.on('keypress', function(event) {
368+
var keyCode = event.which || event.keyCode;
369+
if (keyCode === 32 || keyCode === 13) {
370+
scope.$apply(callback);
371+
}
369372

370-
function callback() {
371-
fn(scope, { $event: event });
372-
}
373-
});
373+
function callback() {
374+
fn(scope, { $event: event });
375+
}
376+
});
377+
}
374378
}
375379
};
376380
}
377381
};
378382
}])
379383
.directive('ngDblclick', ['$aria', function($aria) {
380384
return function(scope, elem, attr) {
381-
if ($aria.config('tabindex') && !elem.attr('tabindex')) {
385+
if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) {
382386
elem.attr('tabindex', 0);
383387
}
384388
};

test/ngAria/ariaSpec.js

+26-21
Original file line numberDiff line numberDiff line change
@@ -616,10 +616,35 @@ describe('$aria', function() {
616616
describe('tabindex', function() {
617617
beforeEach(injectScopeAndCompiler);
618618

619-
it('should attach tabindex to role="checkbox", ng-click, and ng-dblclick', function() {
619+
it('should not attach to native controls', function() {
620+
var element = [
621+
$compile("<button ng-click='something'></button>")(scope),
622+
$compile("<a ng-href='#/something'>")(scope),
623+
$compile("<input ng-model='val'>")(scope),
624+
$compile("<textarea ng-model='val'></textarea>")(scope),
625+
$compile("<select ng-model='val'></select>")(scope),
626+
$compile("<details ng-model='val'></details>")(scope)
627+
];
628+
expectAriaAttrOnEachElement(element, 'tabindex', undefined);
629+
});
630+
631+
it('should not attach to random ng-model elements', function() {
632+
compileElement('<div ng-model="val"></div>');
633+
expect(element.attr('tabindex')).toBeUndefined();
634+
});
635+
636+
it('should attach tabindex to custom inputs', function() {
637+
compileElement('<div type="checkbox" ng-model="val"></div>');
638+
expect(element.attr('tabindex')).toBe('0');
639+
620640
compileElement('<div role="checkbox" ng-model="val"></div>');
621641
expect(element.attr('tabindex')).toBe('0');
622642

643+
compileElement('<div type="range" ng-model="val"></div>');
644+
expect(element.attr('tabindex')).toBe('0');
645+
});
646+
647+
it('should attach to ng-click and ng-dblclick', function() {
623648
compileElement('<div ng-click="someAction()"></div>');
624649
expect(element.attr('tabindex')).toBe('0');
625650

@@ -640,26 +665,6 @@ describe('$aria', function() {
640665
compileElement('<div ng-dblclick="someAction()" tabindex="userSetValue"></div>');
641666
expect(element.attr('tabindex')).toBe('userSetValue');
642667
});
643-
644-
it('should set proper tabindex values for radiogroup', function() {
645-
compileElement('<div role="radiogroup">' +
646-
'<div role="radio" ng-model="val" value="one">1</div>' +
647-
'<div role="radio" ng-model="val" value="two">2</div>' +
648-
'</div>');
649-
650-
var one = element.contents().eq(0);
651-
var two = element.contents().eq(1);
652-
653-
scope.$apply("val = 'one'");
654-
expect(one.attr('tabindex')).toBe('0');
655-
expect(two.attr('tabindex')).toBe('-1');
656-
657-
scope.$apply("val = 'two'");
658-
expect(one.attr('tabindex')).toBe('-1');
659-
expect(two.attr('tabindex')).toBe('0');
660-
661-
dealoc(element);
662-
});
663668
});
664669

665670
describe('accessible actions', function() {

0 commit comments

Comments
 (0)