diff --git a/config/test-utils.js b/config/test-utils.js index 6f56ab9e3e8..953bfbffc21 100644 --- a/config/test-utils.js +++ b/config/test-utils.js @@ -32,6 +32,23 @@ var TestUtil = { test.after(function() { angular.element.prototype.focus = focus; }); - } + }, + + /** + * Create a fake version of $$rAF that does things asynchronously + */ + mockRaf: function() { + module('ng', function($provide) { + $provide.value('$$rAF', mockRaf); + function mockRaf(cb) { + cb(); + } + mockRaf.debounce = function(cb) { + return function() { + cb.apply(this, arguments); + }; + }; + }); + } }; diff --git a/src/components/buttons/buttons.js b/src/components/buttons/buttons.js index 1aea3b96062..7e373fb1fdb 100644 --- a/src/components/buttons/buttons.js +++ b/src/components/buttons/buttons.js @@ -95,7 +95,7 @@ function MaterialButtonDirective(ngHrefDirectives, $materialInkRipple, $material }); return function postLink(scope, element, attr) { - $materialAria.expect(element, 'aria-label', element.text()); + $materialAria.expect(element, 'aria-label', true); $materialInkRipple.attachButtonBehavior(element); }; } diff --git a/src/components/buttons/buttons.spec.js b/src/components/buttons/buttons.spec.js index b1ae1122a8e..2d5df49b91b 100644 --- a/src/components/buttons/buttons.spec.js +++ b/src/components/buttons/buttons.spec.js @@ -1,5 +1,6 @@ describe('material-button', function() { + beforeEach(TestUtil.mockRaf); beforeEach(module('material.components.button')); it('should have inner-anchor with attrs if href attr is given', inject(function($compile, $rootScope) { diff --git a/src/components/buttons/demo1/index.html b/src/components/buttons/demo1/index.html index f3b940798ca..d0cc1ad52fb 100644 --- a/src/components/buttons/demo1/index.html +++ b/src/components/buttons/demo1/index.html @@ -42,19 +42,19 @@
- + - + - + - + diff --git a/src/components/checkbox/checkbox.js b/src/components/checkbox/checkbox.js index 1d2bc4fb8b5..58dd3777ed1 100644 --- a/src/components/checkbox/checkbox.js +++ b/src/components/checkbox/checkbox.js @@ -76,8 +76,6 @@ function MaterialCheckboxDirective(inputDirectives, $materialInkRipple, $materia tAttrs.tabIndex = 0; tElement.attr('role', tAttrs.type); - $materialAria.expect(tElement, 'aria-label', tElement.text()); - return function postLink(scope, element, attr, ngModelCtrl) { var checked = false; @@ -90,6 +88,8 @@ function MaterialCheckboxDirective(inputDirectives, $materialInkRipple, $materia $formatters: [] }; + $materialAria.expect(element, 'aria-label', true); + // Reuse the original input[type=checkbox] directive from Angular core. // This is a bit hacky as we need our own event listener and own render // function. @@ -120,7 +120,6 @@ function MaterialCheckboxDirective(inputDirectives, $materialInkRipple, $materia function render() { checked = ngModelCtrl.$viewValue; - // element.attr('aria-checked', checked); if(checked) { element.addClass(CHECKED_CSS); } else { diff --git a/src/components/checkbox/checkbox.spec.js b/src/components/checkbox/checkbox.spec.js index 93ebfd0de67..b1b54b60069 100644 --- a/src/components/checkbox/checkbox.spec.js +++ b/src/components/checkbox/checkbox.spec.js @@ -4,6 +4,29 @@ describe('materialCheckbox', function() { beforeEach(module('material.components.checkbox')); beforeEach(module('ngAria')); + beforeEach(TestUtil.mockRaf); + + it('should warn developers they need a label', inject(function($compile, $rootScope, $log){ + spyOn($log, "warn"); + var element = $compile('
' + + '' + + '' + + '
')($rootScope); + + var cbElements = element.find('material-checkbox'); + expect($log.warn).toHaveBeenCalled(); + })); + + it('should copy text content to aria-label', inject(function($compile, $rootScope){ + var element = $compile('
' + + '' + + 'Some text' + + '' + + '
')($rootScope); + + var cbElements = element.find('material-checkbox'); + expect(cbElements.eq(0).attr('aria-label')).toBe('Some text'); + })); it('should set checked css class and aria-checked attributes', inject(function($compile, $rootScope) { var element = $compile('
' + diff --git a/src/components/dialog/dialog.js b/src/components/dialog/dialog.js index 2c21ba54b8d..e982a1a1ce3 100644 --- a/src/components/dialog/dialog.js +++ b/src/components/dialog/dialog.js @@ -247,6 +247,6 @@ function MaterialDialogService($timeout, $rootElement, $materialEffects, $animat dialogContent = element; } var defaultText = Util.stringFromTextBody(dialogContent.text(), 3); - $materialAria.expect(element, 'aria-label', defaultText); + $materialAria.expect(element, 'aria-label', true, defaultText); } } diff --git a/src/components/dialog/dialog.spec.js b/src/components/dialog/dialog.spec.js index 73a277d9771..32a39b800a3 100644 --- a/src/components/dialog/dialog.spec.js +++ b/src/components/dialog/dialog.spec.js @@ -1,5 +1,6 @@ describe('$materialDialog', function() { + beforeEach(TestUtil.mockRaf); beforeEach(module('material.components.dialog', 'ngAnimateMock')); beforeEach(inject(function spyOnMaterialEffects($materialEffects, $$q, $animate) { diff --git a/src/components/radioButton/radioButton.js b/src/components/radioButton/radioButton.js index 89ecdd2f7fe..6dabd429a55 100644 --- a/src/components/radioButton/radioButton.js +++ b/src/components/radioButton/radioButton.js @@ -252,7 +252,7 @@ function materialRadioButtonDirective($materialAria) { 'aria-checked' : 'false' }); - $materialAria.expect(element, 'aria-label', element.text()); + $materialAria.expect(element, 'aria-label', true); /** * Build a unique ID for each radio button that will be used with aria-activedescendant. diff --git a/src/components/radioButton/radioButton.spec.js b/src/components/radioButton/radioButton.spec.js index ba7b6afa9b1..b2f9e0f47e8 100644 --- a/src/components/radioButton/radioButton.spec.js +++ b/src/components/radioButton/radioButton.spec.js @@ -1,6 +1,7 @@ describe('radioButton', function() { var CHECKED_CSS = 'material-checked'; + beforeEach(TestUtil.mockRaf); beforeEach(module('material.components.radioButton')); it('should set checked css class', inject(function($compile, $rootScope) { @@ -31,7 +32,7 @@ describe('radioButton', function() { expect(rbGroupElement.find('material-radio-button').eq(0).attr('role')).toEqual('radio'); })); - it('should set aria attributes', inject(function($compile, $rootScope) { + it('should set aria states', inject(function($compile, $rootScope) { var element = $compile('' + '' + '' + @@ -50,6 +51,26 @@ describe('radioButton', function() { expect(element.attr('aria-activedescendant')).not.toEqual(rbElements.eq(0).attr('id')); })); + it('should warn developers they need a label', inject(function($compile, $rootScope, $log){ + spyOn($log, "warn"); + var element = $compile('' + + '' + + '' + + '')($rootScope); + + expect($log.warn).toHaveBeenCalled(); + })); + + it('should create an aria label from provided text', inject(function($compile, $rootScope) { + var element = $compile('' + + 'Blue' + + 'Green' + + '')($rootScope); + + var rbElements = element.find('material-radio-button'); + expect(rbElements.eq(0).attr('aria-label')).toEqual('Blue'); + })); + it('should be operable via arrow keys', inject(function($compile, $rootScope) { var element = $compile('' + '' + diff --git a/src/components/slider/slider.js b/src/components/slider/slider.js index 26d7c994a5c..b9f4df04a12 100644 --- a/src/components/slider/slider.js +++ b/src/components/slider/slider.js @@ -106,6 +106,7 @@ function SliderController(scope, element, attr, $$rAF, $window, $materialEffects var trackContainer = angular.element(element[0].querySelector('.slider-track-container')); var activeTrack = angular.element(element[0].querySelector('.slider-track-fill')); var tickContainer = angular.element(element[0].querySelector('.slider-track-ticks')); + var throttledRefreshDimensions = Util.throttle(refreshSliderDimensions, 5000); // Default values, overridable by attrs attr.min ? attr.$observe('min', updateMin) : updateMin(0); @@ -122,7 +123,8 @@ function SliderController(scope, element, attr, $$rAF, $window, $materialEffects updateAriaDisabled(!!attr.disabled); } - $materialAria.expect(element, 'aria-label'); + $materialAria.expect(element, 'aria-label', false); + element.attr('tabIndex', 0); element.attr('role', 'slider'); element.on('keydown', keydownListener); @@ -209,7 +211,6 @@ function SliderController(scope, element, attr, $$rAF, $window, $materialEffects * Refreshing Dimensions */ var sliderDimensions = {}; - var throttledRefreshDimensions = Util.throttle(refreshSliderDimensions, 5000); refreshSliderDimensions(); function refreshSliderDimensions() { sliderDimensions = trackContainer[0].getBoundingClientRect(); diff --git a/src/components/slider/slider.spec.js b/src/components/slider/slider.spec.js index b3868b98389..e49934a2e6e 100644 --- a/src/components/slider/slider.spec.js +++ b/src/components/slider/slider.spec.js @@ -12,6 +12,8 @@ describe('material-slider', function() { }; } + beforeEach(TestUtil.mockRaf); + beforeEach(module('ngAria')); beforeEach(module('material.components.slider','material.decorators')); it('should set model on press', inject(function($compile, $rootScope, $timeout) { @@ -93,4 +95,18 @@ describe('material-slider', function() { expect($rootScope.model).toBe(100); })); + it('should warn developers they need a label', inject(function($compile, $rootScope, $timeout, $log) { + spyOn($log, "warn"); + + var element = $compile( + '
' + + '' + + '' + + '
' + )($rootScope); + + var sliders = element.find('material-slider'); + expect($log.warn).toHaveBeenCalledWith(sliders[0]); + expect($log.warn).not.toHaveBeenCalledWith(sliders[1]); + })); }); diff --git a/src/components/switch/switch.spec.js b/src/components/switch/switch.spec.js index 3fa8bee099d..db1bbf0a73c 100644 --- a/src/components/switch/switch.spec.js +++ b/src/components/switch/switch.spec.js @@ -1,6 +1,7 @@ describe('', function() { var CHECKED_CSS = 'material-checked'; + beforeEach(TestUtil.mockRaf); beforeEach(module('ngAria')); beforeEach(module('material.components.switch')); diff --git a/src/components/tabs/js/tabItemDirective.js b/src/components/tabs/js/tabItemDirective.js index 61219f736ca..623bf1a9237 100644 --- a/src/components/tabs/js/tabItemDirective.js +++ b/src/components/tabs/js/tabItemDirective.js @@ -200,7 +200,7 @@ function MaterialTabDirective($materialInkRipple, $compile, $materialAria) { 'aria-labelledby': tabId }); - $materialAria.expect(element, 'aria-label', element.text()); + $materialAria.expect(element, 'aria-label', true); } }; diff --git a/src/components/tabs/tabs.spec.js b/src/components/tabs/tabs.spec.js index 2fb3d3dda30..f1485d3103b 100644 --- a/src/components/tabs/tabs.spec.js +++ b/src/components/tabs/tabs.spec.js @@ -1,5 +1,6 @@ describe('materialTabs directive', function() { + beforeEach(TestUtil.mockRaf); beforeEach(module('material.components.tabs', 'material.decorators', 'material.services.aria')); describe('controller', function(){ diff --git a/src/services/aria/aria.js b/src/services/aria/aria.js index 5bf4a16a229..60a1ca9ddff 100644 --- a/src/services/aria/aria.js +++ b/src/services/aria/aria.js @@ -1,12 +1,13 @@ angular.module('material.services.aria', []) .service('$materialAria', [ + '$$rAF', '$log', AriaService ]); -function AriaService($log) { - var messageTemplate = 'ARIA: Attribute "%s", required for accessibility, is missing on "%s"!'; +function AriaService($$rAF, $log) { + var messageTemplate = 'ARIA: Attribute "%s", required for accessibility, is missing on "%s"'; var defaultValueTemplate = 'Default value was set: %s="%s".'; return { @@ -17,23 +18,31 @@ function AriaService($log) { * Check if expected ARIA has been specified on the target element * @param element * @param attrName - * @param defaultValue + * @param copyElementText + * @param {optional} defaultValue */ - function expectAttribute(element, attrName, defaultValue) { - - var node = element[0]; - if (!node.hasAttribute(attrName)) { - var hasDefault = angular.isDefined(defaultValue); - - if (hasDefault) { - defaultValue = String(defaultValue).trim(); - // $log.warn(messageTemplate + ' ' + defaultValueTemplate, - // attrName, getTagString(node), attrName, defaultValue); - element.attr(attrName, defaultValue); - } else { - // $log.warn(messageTemplate, attrName, getTagString(node)); + function expectAttribute(element, attrName, copyElementText, defaultValue) { + + $$rAF(function(){ + + var node = element[0]; + if (!node.hasAttribute(attrName)) { + + var hasDefault; + if(copyElementText === true){ + if(!defaultValue) defaultValue = element.text().trim(); + hasDefault = angular.isDefined(defaultValue) && defaultValue.length; + } + + if (hasDefault) { + defaultValue = String(defaultValue).trim(); + element.attr(attrName, defaultValue); + } else { + $log.warn(messageTemplate, attrName, node); + $log.warn(node); + } } - } + }); }