diff --git a/Gruntfile.js b/Gruntfile.js index fb934d0b7850..84a6873c0dda 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -147,6 +147,9 @@ module.exports = function(grunt) { }, ngTouch: { files: { src: 'src/ngTouch/**/*.js' }, + }, + ngAria: { + files: {src: 'src/ngAria/**/*.js'}, } }, @@ -214,6 +217,10 @@ module.exports = function(grunt) { dest: 'build/angular-cookies.js', src: util.wrap(files['angularModules']['ngCookies'], 'module') }, + aria: { + dest: 'build/angular-aria.js', + src: util.wrap(files['angularModules']['ngAria'], 'module') + }, "promises-aplus-adapter": { dest:'tmp/promises-aplus-adapter++.js', src:['src/ng/q.js','lib/promises-aplus/promises-aplus-test-adapter.js'] @@ -230,7 +237,8 @@ module.exports = function(grunt) { touch: 'build/angular-touch.js', resource: 'build/angular-resource.js', route: 'build/angular-route.js', - sanitize: 'build/angular-sanitize.js' + sanitize: 'build/angular-sanitize.js', + aria: 'build/angular-aria.js' }, diff --git a/angularFiles.js b/angularFiles.js index b1d5fd1c5527..c1af7c8d581e 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -106,6 +106,9 @@ var angularFiles = { 'src/ngTouch/directive/ngClick.js', 'src/ngTouch/directive/ngSwipe.js' ], + 'ngAria': [ + 'src/ngAria/aria.js' + ] }, 'angularScenario': [ @@ -139,7 +142,8 @@ var angularFiles = { 'test/ngRoute/**/*.js', 'test/ngSanitize/**/*.js', 'test/ngMock/*.js', - 'test/ngTouch/**/*.js' + 'test/ngTouch/**/*.js', + 'test/ngAria/*.js' ], 'karma': [ @@ -173,7 +177,8 @@ var angularFiles = { 'test/ngRoute/**/*.js', 'test/ngResource/*.js', 'test/ngSanitize/**/*.js', - 'test/ngTouch/**/*.js' + 'test/ngTouch/**/*.js', + 'test/ngAria/*.js' ], 'karmaJquery': [ @@ -201,7 +206,8 @@ angularFiles['angularSrcModules'] = [].concat( angularFiles['angularModules']['ngRoute'], angularFiles['angularModules']['ngSanitize'], angularFiles['angularModules']['ngMock'], - angularFiles['angularModules']['ngTouch'] + angularFiles['angularModules']['ngTouch'], + angularFiles['angularModules']['ngAria'] ); if (exports) { diff --git a/src/ngAria/aria.js b/src/ngAria/aria.js new file mode 100644 index 000000000000..a4b924683ef2 --- /dev/null +++ b/src/ngAria/aria.js @@ -0,0 +1,284 @@ +'use strict'; + +/** + * @ngdoc module + * @name ngAria + * @description + * + * The `ngAria` module provides support for to embed aria tags that convey state or semantic information + * about the application in order to allow assistive technologies to convey appropriate information to + * persons with disabilities. + * + *
+ * + * # Usage + * To enable the addition of the aria tags, just require the module into your application and the tags will + * hook into your ng-show/ng-hide, input, textarea, button, select and ng-required directives and adds the + * appropriate aria-tags. + * + * Currently, the following aria tags are implemented: + * + * + aria-hidden + * + aria-checked + * + aria-disabled + * + aria-required + * + aria-invalid + * + aria-multiline + * + aria-valuenow + * + aria-valuemin + * + aria-valuemax + * + tabindex + * + * You can disable individual aria tags by using the {@link ngAria.$ariaProvider#config config} method. + */ + + /* global -ngAriaModule */ +var ngAriaModule = angular.module('ngAria', ['ng']). + provider('$aria', $AriaProvider); + +/** + * @ngdoc provider + * @name $ariaProvider + * + * @description + * + * Used for configuring aria attributes. + * + * ## Dependencies + * Requires the {@link ngAria `ngAria`} module to be installed. + */ +function $AriaProvider(){ + var config = { + ariaHidden : true, + ariaChecked: true, + ariaDisabled: true, + ariaRequired: true, + ariaInvalid: true, + ariaMultiline: true, + ariaValue: true, + tabindex: true + }; + + /** + * @ngdoc method + * @name $ariaProvider#config + * + * @param {object} config object to enable/disable specific aria tags + * + * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags + * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags + * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags + * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags + * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags + * - **ariaMultiline** – `{boolean}` – Enables/disables aria-multiline tags + * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and aria-valuenow tags + * - **tabindex** – `{boolean}` – Enables/disables tabindex tags + * + * @description + * Enables/disables various aria tags + */ + this.config = function(newConfig){ + config = angular.extend(config, newConfig); + }; + + var convertCase = function(input){ + return input.replace(/[A-Z]/g, function(letter, pos){ + return (pos ? '-' : '') + letter.toLowerCase(); + }); + }; + + var watchAttr = function(attrName, ariaName){ + return function(scope, elem, attr){ + if(config[ariaName] && !elem.attr(convertCase(ariaName))){ + if(attr[attrName]){ + elem.attr(convertCase(ariaName), true); + } + var destroyWatcher = attr.$observe(attrName, function(newVal){ + elem.attr(convertCase(ariaName), !angular.isUndefined(newVal)); + }); + scope.$on('$destroy', function(){ + destroyWatcher(); + }); + } + }; + }; + + var watchClass = function(className, ariaName){ + return function(scope, elem, attr){ + if(config[ariaName] && !elem.attr(convertCase(ariaName))){ + var destroyWatcher = scope.$watch(function(){ + return elem.attr('class'); + }, function(){ + elem.attr(convertCase(ariaName), elem.hasClass(className)); + }); + scope.$on('$destroy', function(){ + destroyWatcher(); + }); + } + }; + }; + + var watchExpr = function(expr, ariaName){ + return function(scope, elem, attr){ + if(config[ariaName] && !elem.attr(convertCase(ariaName))){ + var destroyWatch; + var destroyObserve = attr.$observe(expr, function(value){ + if(angular.isFunction(destroyWatch)){ + destroyWatch(); + } + destroyWatch = scope.$watch(value, function(newVal){ + elem.attr(convertCase(ariaName), newVal); + }); + }); + scope.$on('$destroy', function(){ + destroyObserve(); + }); + } + }; + }; + + this.$get = function(){ + return { + ariaHidden: watchClass('ng-hide', 'ariaHidden'), + ariaChecked: watchExpr('ngModel', 'ariaChecked'), + ariaDisabled: watchExpr('ngDisabled', 'ariaDisabled'), + ariaNgRequired: watchExpr('ngRequired', 'ariaRequired'), + ariaRequired: watchAttr('required', 'ariaRequired'), + ariaInvalid: watchClass('ng-invalid', 'ariaInvalid'), + ariaValue: function(scope, elem, attr, ngModel){ + if(config.ariaValue){ + if(attr.min && !elem.attr('aria-valuemin')){ + elem.attr('aria-valuemin', attr.min); + } + if(attr.max && !elem.attr('aria-valuemax')){ + elem.attr('aria-valuemax', attr.max); + } + if(ngModel && !elem.attr('aria-valuenow')){ + var destroyWatcher = scope.$watch(function(){ + return ngModel.$modelValue; + }, function(newVal){ + elem.attr('aria-valuenow', newVal); + }); + scope.$on('$destroy', function(){ + destroyWatcher(); + }); + } + } + }, + radio: function(scope, elem, attr, ngModel){ + if(config.ariaChecked && ngModel && !elem.attr('aria-checked')){ + var needsTabIndex = config.tabindex && !elem.attr('tabindex'); + var destroyWatcher = scope.$watch(function(){ + return ngModel.$modelValue; + }, function(newVal){ + if(newVal === attr.value){ + elem.attr('aria-checked', true); + if(needsTabIndex){ + elem.attr('tabindex', 0); + } + }else{ + elem.attr('aria-checked', false); + if(needsTabIndex){ + elem.attr('tabindex', -1); + } + } + }); + scope.$on('$destroy', function(){ + destroyWatcher(); + }); + } + }, + multiline: function(scope, elem, attr){ + if(config.ariaMultiline && !elem.attr('aria-multiline')){ + elem.attr('aria-multiline', true); + } + }, + roleChecked: function(scope, elem, attr){ + if(config.ariaChecked && attr.checked && !elem.attr('aria-checked')){ + elem.attr('aria-checked', true); + } + }, + tabindex: function(scope, elem, attr){ + if(config.tabindex && !elem.attr('tabindex')){ + elem.attr('tabindex', 0); + } + } + }; + }; +} + +ngAriaModule.directive('ngShow', ['$aria', function($aria){ + return $aria.ariaHidden; +}]).directive('ngHide', ['$aria', function($aria){ + return $aria.ariaHidden; +}]).directive('input', ['$aria', function($aria){ + return{ + restrict: 'E', + require: '?ngModel', + link: function(scope, elem, attr, ngModel){ + if(attr.type === 'checkbox'){ + $aria.ariaChecked(scope, elem, attr); + } + if(attr.type === 'radio'){ + $aria.radio(scope, elem, attr, ngModel); + } + $aria.ariaRequired(scope, elem, attr); + $aria.ariaInvalid(scope, elem, attr); + if(attr.type === 'range'){ + $aria.ariaValue(scope, elem, attr, ngModel); + } + } + }; +}]).directive('textarea', ['$aria', function($aria){ + return{ + restrict: 'E', + link: function(scope, elem, attr){ + $aria.ariaRequired(scope, elem, attr); + $aria.ariaInvalid(scope, elem, attr); + $aria.multiline(scope, elem, attr); + } + }; +}]).directive('select', ['$aria', function($aria){ + return{ + restrict: 'E', + link: function(scope, elem, attr){ + $aria.ariaRequired(scope, elem, attr); + } + }; +}]) +.directive('ngRequired', ['$aria', function($aria){ + return $aria.ariaNgRequired; +}]) +.directive('ngDisabled', ['$aria', function($aria){ + return $aria.ariaDisabled; +}]) +.directive('role', ['$aria', function($aria){ + return{ + restrict: 'A', + require: '?ngModel', + link: function(scope, elem, attr, ngModel){ + if(attr.role === 'textbox'){ + $aria.multiline(scope, elem, attr); + } + if(attr.role === "progressbar" || attr.role === "slider"){ + $aria.ariaValue(scope, elem, attr, ngModel); + } + if(attr.role === "checkbox" || attr.role === "menuitemcheckbox"){ + $aria.roleChecked(scope, elem, attr); + $aria.tabindex(scope, elem, attr); + } + if(attr.role === "radio" || attr.role === "menuitemradio"){ + $aria.radio(scope, elem, attr, ngModel); + } + if(attr.role === "button"){ + $aria.tabindex(scope, elem, attr); + } + } + }; +}]) +.directive('ngClick', ['$aria', function($aria){ + return $aria.tabindex; +}]) +.directive('ngDblclick', ['$aria', function($aria){ + return $aria.tabindex; +}]); diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js new file mode 100644 index 000000000000..c31631858a27 --- /dev/null +++ b/test/ngAria/ariaSpec.js @@ -0,0 +1,447 @@ +'use strict'; + +function expectAriaAttr(elem, ariaAttr, expected){ + angular.forEach(elem, function(val){ + expect(angular.element(val).attr(ariaAttr)).toBe(expected); + }); +} + +describe('$aria', function(){ + + describe('aria-hidden', function(){ + beforeEach(module('ngAria')); + + it('should attach aria-hidden to ng-show', inject(function($compile, $rootScope){ + var element = $compile("
")($rootScope); + $rootScope.$apply('val=false'); + expectAriaAttr(element, 'aria-hidden', 'true'); + $rootScope.$apply('val=true'); + expectAriaAttr(element, 'aria-hidden', 'false'); + })); + + it('should attach aria-hidden to ng-hide', inject(function($compile, $rootScope){ + var element = $compile("
")($rootScope); + $rootScope.$apply('val=false'); + expectAriaAttr(element, 'aria-hidden', 'false'); + $rootScope.$apply('val=true'); + expectAriaAttr(element, 'aria-hidden', 'true'); + })); + + it('should not attach if an aria-hidden is already present', inject(function($compile, $rootScope){ + var element = [ + $compile('
')($rootScope), + $compile('
')($rootScope) + ]; + $rootScope.$apply('val=true'); + expectAriaAttr(element, 'aria-hidden', 'userSetValue'); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + ariaHidden : false + }); + }); + + module('ariaTest'); + }); + + it('should not attach aria-hidden if the option is disabled', inject(function($compile, $rootScope){ + var element = [ + $compile("
")($rootScope), + $compile("
")($rootScope) + ]; + $rootScope.$apply('val=false'); + expectAriaAttr(element, 'aria-hidden', undefined); + })); + }); + }); + + describe('aria-checked', function(){ + beforeEach(module('ngAria')); + + it('should attach itself to input type=checkbox', inject(function($compile, $rootScope){ + var element = $compile("")($rootScope); + $rootScope.$apply('val=true'); + expectAriaAttr(element, 'aria-checked', 'true'); + $rootScope.$apply('val=false'); + expectAriaAttr(element, 'aria-checked', 'false'); + })); + + it('should attach itself to input type=radio', inject(function($compile, $rootScope){ + var element = $compile("")($rootScope); + $rootScope.$apply("val='one'"); + expect(angular.element(element).eq(0).attr('aria-checked')).toBe('true'); + expect(angular.element(element).eq(1).attr('aria-checked')).toBe('false'); + + $rootScope.$apply("val='two'"); + expect(angular.element(element).eq(0).attr('aria-checked')).toBe('false'); + expect(angular.element(element).eq(1).attr('aria-checked')).toBe('true'); + })); + + it('should attach itself to role="radio", role="checkbox", role="menuitemradio" and role="menuitemcheckbox"', inject(function($compile, $rootScope){ + var element = [ + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope) + ]; + $rootScope.$apply("val='one'"); + expectAriaAttr(element, 'aria-checked', 'true'); + })); + + it('should not attach itself if an aria-checked value is already present', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope) + ]; + $rootScope.$apply("val1=true;val2='one';val3='1'"); + expectAriaAttr(element, 'aria-checked', 'userSetValue'); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + ariaChecked : false + }); + }); + + module('ariaTest'); + }); + + it('should not attach aria-checked if the option is disabled', inject(function($compile, $rootScope){ + var element = [ + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope) + ]; + $rootScope.$digest(); + expectAriaAttr(element, 'aria-checked', undefined); + })); + }); + }); + + describe('aria-disabled', function(){ + beforeEach(module('ngAria')); + + it('should attach itself to input, textarea, button and select', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope) + ]; + $rootScope.$apply('val=false'); + expectAriaAttr(element, 'aria-disabled', 'false'); + + $rootScope.$apply('val=true'); + expectAriaAttr(element, 'aria-disabled', 'true'); + })); + + it('should not attach itself if an aria tag is already present', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope) + ]; + + $rootScope.$apply('val=true'); + expectAriaAttr(element, 'aria-disabled', 'userSetValue'); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + ariaDisabled : false + }); + }); + + module('ariaTest'); + }); + + it('should not attach aria-disabled if the option is disabled', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope) + ]; + + $rootScope.$apply('val=false'); + expectAriaAttr(element, 'aria-disabled', undefined); + })); + }); + }); + + describe('aria-invalid', function(){ + beforeEach(module('ngAria')); + + it('should attach aria-invalid to input', inject(function($compile, $rootScope){ + var element = $compile("")($rootScope); + $rootScope.$apply("txtInput='LTten'"); + expectAriaAttr(element, 'aria-invalid', 'true'); + + $rootScope.$apply("txtInput='morethantencharacters'"); + expectAriaAttr(element, 'aria-invalid', 'false'); + })); + + it('should not attach itself if aria-invalid is already present', inject(function($compile, $rootScope){ + var element = $compile("")($rootScope); + $rootScope.$apply("txtInput='LTten'"); + expectAriaAttr(element, 'aria-invalid', 'userSetValue'); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + ariaInvalid : false + }); + }); + + module('ariaTest'); + }); + + it('should not attach aria-invalid if the option is disabled', inject(function($compile, $rootScope){ + var element = $compile("")($rootScope); + $rootScope.$apply("txtInput='LTten'"); + expectAriaAttr(element, 'aria-invalid', undefined); + })); + }); + }); + + describe('aria-required', function(){ + beforeEach(module('ngAria')); + + it('should attach aria-required to input, textarea, select and ngRequired', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope) + ]; + $rootScope.$digest(); + expectAriaAttr(element, 'aria-required', 'true'); + + element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope) + ]; + $rootScope.$apply("val='input is valid now'"); + expectAriaAttr(element, 'aria-required', 'false'); + })); + + it('should not attach itself if aria-required is already present', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope) + ]; + + $rootScope.$digest(); + expectAriaAttr(element, 'aria-required', 'userSetValue'); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + ariaRequired : false + }); + }); + + module('ariaTest'); + }); + + it('should not attach aria-required when the option is disabled', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("")($rootScope), + $compile("")($rootScope) + ]; + + $rootScope.$digest(); + expectAriaAttr(element, 'aria-required', undefined); + })); + }); + }); + + describe('aria-multiline', function(){ + beforeEach(module('ngAria')); + + it('should attach aria-multiline to textbox and role="textbox"', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("
")($rootScope) + ]; + + $rootScope.$digest(); + expectAriaAttr(element, 'aria-multiline', 'true'); + })); + + it('should not attach if aria-multiline is already present', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("
")($rootScope) + ]; + + $rootScope.$digest(); + expectAriaAttr(element, 'aria-multiline', 'userSetValue'); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + ariaMultiline : false + }); + }); + + module('ariaTest'); + }); + + it('should not attach itself to textbox or role="textbox" when disabled', inject(function($compile, $rootScope){ + var element = [ + $compile("")($rootScope), + $compile("
")($rootScope) + ]; + + $rootScope.$digest(); + expectAriaAttr(element, 'aria-multiline', undefined); + })); + }); + }); + + describe('aria-value', function(){ + beforeEach(module('ngAria')); + + it('should attach to input type="range"', inject(function($compile, $rootScope){ + var element = [ + $compile('')($rootScope), + $compile('
')($rootScope), + $compile('
')($rootScope) + ]; + + $rootScope.$apply('val=50'); + expectAriaAttr(element, 'aria-valuenow', "50"); + expectAriaAttr(element, 'aria-valuemin', "0"); + expectAriaAttr(element, 'aria-valuemax', "100"); + + $rootScope.$apply('val=90'); + expectAriaAttr(element, 'aria-valuenow', "90"); + })); + + it('should not attach if aria-value* is already present', inject(function($compile, $rootScope){ + var element = [ + $compile('')($rootScope), + $compile('
')($rootScope), + $compile('
')($rootScope) + ]; + + $rootScope.$apply('val=50'); + expectAriaAttr(element, 'aria-valuenow', "userSetValue1"); + expectAriaAttr(element, 'aria-valuemin', "userSetValue2"); + expectAriaAttr(element, 'aria-valuemax', "userSetValue3"); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + ariaValue : false + }); + }); + + module('ariaTest'); + }); + + it('should not attach itself when the option is disabled', inject(function($compile, $rootScope){ + var element = [ + $compile('')($rootScope), + $compile('
')($rootScope) + ]; + + $rootScope.$apply('val=50'); + expectAriaAttr(element, 'aria-valuenow', undefined); + expectAriaAttr(element, 'aria-valuemin', undefined); + expectAriaAttr(element, 'aria-valuemax', undefined); + })); + }); + }); + + describe('tabindex', function(){ + beforeEach(module('ngAria')); + + it('should attach tabindex to role=button, role=checkbox, ng-click and ng-dblclick', inject(function($compile, $rootScope){ + var element = [ + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope) + ]; + $rootScope.$digest(); + + expectAriaAttr(element, 'tabindex', '0'); + })); + + it('should not attach tabindex to role=button, role=checkbox and ng-click if they are already present', inject(function($compile, $rootScope){ + var element = [ + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope) + ]; + $rootScope.$digest(); + + expectAriaAttr(element, 'tabindex', 'userSetValue'); + })); + + it('should set proper tabindex values for radiogroup', inject(function($compile, $rootScope){ + var element = $compile("
1
2
")($rootScope); + + $rootScope.$apply("val='one'"); + expect(angular.element(angular.element(element).children()[0]).attr('tabindex')).toBe('0'); + expect(angular.element(angular.element(element).children()[1]).attr('tabindex')).toBe('-1'); + + $rootScope.$apply("val='two'"); + expect(angular.element(angular.element(element).children()[0]).attr('tabindex')).toBe('-1'); + expect(angular.element(angular.element(element).children()[1]).attr('tabindex')).toBe('0'); + + dealoc(element); + })); + + describe('disabled', function(){ + beforeEach(function(){ + angular.module('ariaTest', ['ngAria']).config(function($ariaProvider){ + $ariaProvider.config({ + tabindex : false + }); + }); + + module('ariaTest'); + }); + it('should not attach when disabled', inject(function($compile, $rootScope){ + var element = [ + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope), + $compile("
")($rootScope) + ]; + $rootScope.$digest(); + expectAriaAttr(element, 'tabindex', undefined); + })); + }); + }); +});