From 44337f63fa94116795e83e3a764a6ba6782809c7 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Fri, 13 Mar 2015 18:30:39 +0100 Subject: [PATCH] fix(ngAria): handle elements with role="checkbox/menuitemcheckbox" Fixes #11317 Closes #11321 --- src/ngAria/aria.js | 157 ++++++++++++++++++++++------------------ test/ngAria/ariaSpec.js | 18 ++++- 2 files changed, 100 insertions(+), 75 deletions(-) diff --git a/src/ngAria/aria.js b/src/ngAria/aria.js index 91a96979d985..ba8fd3530760 100644 --- a/src/ngAria/aria.js +++ b/src/ngAria/aria.js @@ -211,88 +211,101 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) { restrict: 'A', require: '?ngModel', priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value - link: function(scope, elem, attr, ngModel) { + compile: function(elem, attr) { var shape = getShape(attr, elem); - var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem); - - function ngAriaWatchModelValue() { - return ngModel.$modelValue; - } - - function getRadioReaction() { - if (needsTabIndex) { - needsTabIndex = false; - return function ngAriaRadioReaction(newVal) { - var boolVal = (attr.value == ngModel.$viewValue); - elem.attr('aria-checked', boolVal); - elem.attr('tabindex', 0 - !boolVal); - }; - } else { - return function ngAriaRadioReaction(newVal) { - elem.attr('aria-checked', (attr.value == ngModel.$viewValue)); - }; - } - } - - function ngAriaCheckboxReaction(newVal) { - elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue)); - } - switch (shape) { - case 'radio': - case 'checkbox': - if (shouldAttachRole(shape, elem)) { - elem.attr('role', shape); - } - if (shouldAttachAttr('aria-checked', 'ariaChecked', elem)) { - scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? - getRadioReaction() : ngAriaCheckboxReaction); + return { + pre: function(scope, elem, attr, ngModel) { + if (shape === 'checkbox' && attr.type !== 'checkbox') { + //Use the input[checkbox] $isEmpty implementation for elements with checkbox roles + ngModel.$isEmpty = function(value) { + return value === false; + }; } - break; - case 'range': - if (shouldAttachRole(shape, elem)) { - elem.attr('role', 'slider'); + }, + post: function(scope, elem, attr, ngModel) { + var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem); + + function ngAriaWatchModelValue() { + return ngModel.$modelValue; } - if ($aria.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 (!elem.attr('aria-valuenow')) { - scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { - elem.attr('aria-valuenow', newVal); - }); + + function getRadioReaction() { + if (needsTabIndex) { + needsTabIndex = false; + return function ngAriaRadioReaction(newVal) { + var boolVal = (attr.value == ngModel.$viewValue); + elem.attr('aria-checked', boolVal); + elem.attr('tabindex', 0 - !boolVal); + }; + } else { + return function ngAriaRadioReaction(newVal) { + elem.attr('aria-checked', (attr.value == ngModel.$viewValue)); + }; } } - break; - case 'multiline': - if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) { - elem.attr('aria-multiline', true); + + function ngAriaCheckboxReaction() { + elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue)); } - break; - } - if (needsTabIndex) { - elem.attr('tabindex', 0); - } + switch (shape) { + case 'radio': + case 'checkbox': + if (shouldAttachRole(shape, elem)) { + elem.attr('role', shape); + } + if (shouldAttachAttr('aria-checked', 'ariaChecked', elem)) { + scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? + getRadioReaction() : ngAriaCheckboxReaction); + } + break; + case 'range': + if (shouldAttachRole(shape, elem)) { + elem.attr('role', 'slider'); + } + if ($aria.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 (!elem.attr('aria-valuenow')) { + scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { + elem.attr('aria-valuenow', newVal); + }); + } + } + break; + case 'multiline': + if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) { + elem.attr('aria-multiline', true); + } + break; + } - if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) { - scope.$watch(function ngAriaRequiredWatch() { - return ngModel.$error.required; - }, function ngAriaRequiredReaction(newVal) { - elem.attr('aria-required', !!newVal); - }); - } + if (needsTabIndex) { + elem.attr('tabindex', 0); + } - if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem)) { - scope.$watch(function ngAriaInvalidWatch() { - return ngModel.$invalid; - }, function ngAriaInvalidReaction(newVal) { - elem.attr('aria-invalid', !!newVal); - }); - } + if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) { + scope.$watch(function ngAriaRequiredWatch() { + return ngModel.$error.required; + }, function ngAriaRequiredReaction(newVal) { + elem.attr('aria-required', !!newVal); + }); + } + + if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem)) { + scope.$watch(function ngAriaInvalidWatch() { + return ngModel.$invalid; + }, function ngAriaInvalidReaction(newVal) { + elem.attr('aria-invalid', !!newVal); + }); + } + } + }; } }; }]) diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js index 8da1f83c7611..e23e3e10d6f5 100644 --- a/test/ngAria/ariaSpec.js +++ b/test/ngAria/ariaSpec.js @@ -155,27 +155,39 @@ describe('$aria', function() { }); it('should attach itself to role="radio"', function() { - scope.$apply("val = 'one'"); - compileElement('
'); + scope.val = 'one'; + compileElement('
'); expect(element.attr('aria-checked')).toBe('true'); + + scope.$apply("val = 'two'"); + expect(element.attr('aria-checked')).toBe('false'); }); it('should attach itself to role="checkbox"', function() { scope.val = true; compileElement('
'); expect(element.attr('aria-checked')).toBe('true'); + + scope.$apply('val = false'); + expect(element.attr('aria-checked')).toBe('false'); }); it('should attach itself to role="menuitemradio"', function() { scope.val = 'one'; - compileElement('
'); + compileElement('
'); expect(element.attr('aria-checked')).toBe('true'); + + scope.$apply("val = 'two'"); + expect(element.attr('aria-checked')).toBe('false'); }); it('should attach itself to role="menuitemcheckbox"', function() { scope.val = true; compileElement('
'); expect(element.attr('aria-checked')).toBe('true'); + + scope.$apply('val = false'); + expect(element.attr('aria-checked')).toBe('false'); }); it('should not attach itself if an aria-checked value is already present', function() {