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

Commit 44337f6

Browse files
Narretzpetebacondarwin
authored andcommitted
fix(ngAria): handle elements with role="checkbox/menuitemcheckbox"
Fixes #11317 Closes #11321
1 parent 4b7a46a commit 44337f6

File tree

2 files changed

+100
-75
lines changed

2 files changed

+100
-75
lines changed

src/ngAria/aria.js

+85-72
Original file line numberDiff line numberDiff line change
@@ -211,88 +211,101 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
211211
restrict: 'A',
212212
require: '?ngModel',
213213
priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value
214-
link: function(scope, elem, attr, ngModel) {
214+
compile: function(elem, attr) {
215215
var shape = getShape(attr, elem);
216-
var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem);
217-
218-
function ngAriaWatchModelValue() {
219-
return ngModel.$modelValue;
220-
}
221-
222-
function getRadioReaction() {
223-
if (needsTabIndex) {
224-
needsTabIndex = false;
225-
return function ngAriaRadioReaction(newVal) {
226-
var boolVal = (attr.value == ngModel.$viewValue);
227-
elem.attr('aria-checked', boolVal);
228-
elem.attr('tabindex', 0 - !boolVal);
229-
};
230-
} else {
231-
return function ngAriaRadioReaction(newVal) {
232-
elem.attr('aria-checked', (attr.value == ngModel.$viewValue));
233-
};
234-
}
235-
}
236-
237-
function ngAriaCheckboxReaction(newVal) {
238-
elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue));
239-
}
240216

241-
switch (shape) {
242-
case 'radio':
243-
case 'checkbox':
244-
if (shouldAttachRole(shape, elem)) {
245-
elem.attr('role', shape);
246-
}
247-
if (shouldAttachAttr('aria-checked', 'ariaChecked', elem)) {
248-
scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
249-
getRadioReaction() : ngAriaCheckboxReaction);
217+
return {
218+
pre: function(scope, elem, attr, ngModel) {
219+
if (shape === 'checkbox' && attr.type !== 'checkbox') {
220+
//Use the input[checkbox] $isEmpty implementation for elements with checkbox roles
221+
ngModel.$isEmpty = function(value) {
222+
return value === false;
223+
};
250224
}
251-
break;
252-
case 'range':
253-
if (shouldAttachRole(shape, elem)) {
254-
elem.attr('role', 'slider');
225+
},
226+
post: function(scope, elem, attr, ngModel) {
227+
var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem);
228+
229+
function ngAriaWatchModelValue() {
230+
return ngModel.$modelValue;
255231
}
256-
if ($aria.config('ariaValue')) {
257-
if (attr.min && !elem.attr('aria-valuemin')) {
258-
elem.attr('aria-valuemin', attr.min);
259-
}
260-
if (attr.max && !elem.attr('aria-valuemax')) {
261-
elem.attr('aria-valuemax', attr.max);
262-
}
263-
if (!elem.attr('aria-valuenow')) {
264-
scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) {
265-
elem.attr('aria-valuenow', newVal);
266-
});
232+
233+
function getRadioReaction() {
234+
if (needsTabIndex) {
235+
needsTabIndex = false;
236+
return function ngAriaRadioReaction(newVal) {
237+
var boolVal = (attr.value == ngModel.$viewValue);
238+
elem.attr('aria-checked', boolVal);
239+
elem.attr('tabindex', 0 - !boolVal);
240+
};
241+
} else {
242+
return function ngAriaRadioReaction(newVal) {
243+
elem.attr('aria-checked', (attr.value == ngModel.$viewValue));
244+
};
267245
}
268246
}
269-
break;
270-
case 'multiline':
271-
if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) {
272-
elem.attr('aria-multiline', true);
247+
248+
function ngAriaCheckboxReaction() {
249+
elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue));
273250
}
274-
break;
275-
}
276251

277-
if (needsTabIndex) {
278-
elem.attr('tabindex', 0);
279-
}
252+
switch (shape) {
253+
case 'radio':
254+
case 'checkbox':
255+
if (shouldAttachRole(shape, elem)) {
256+
elem.attr('role', shape);
257+
}
258+
if (shouldAttachAttr('aria-checked', 'ariaChecked', elem)) {
259+
scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
260+
getRadioReaction() : ngAriaCheckboxReaction);
261+
}
262+
break;
263+
case 'range':
264+
if (shouldAttachRole(shape, elem)) {
265+
elem.attr('role', 'slider');
266+
}
267+
if ($aria.config('ariaValue')) {
268+
if (attr.min && !elem.attr('aria-valuemin')) {
269+
elem.attr('aria-valuemin', attr.min);
270+
}
271+
if (attr.max && !elem.attr('aria-valuemax')) {
272+
elem.attr('aria-valuemax', attr.max);
273+
}
274+
if (!elem.attr('aria-valuenow')) {
275+
scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) {
276+
elem.attr('aria-valuenow', newVal);
277+
});
278+
}
279+
}
280+
break;
281+
case 'multiline':
282+
if (shouldAttachAttr('aria-multiline', 'ariaMultiline', elem)) {
283+
elem.attr('aria-multiline', true);
284+
}
285+
break;
286+
}
280287

281-
if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) {
282-
scope.$watch(function ngAriaRequiredWatch() {
283-
return ngModel.$error.required;
284-
}, function ngAriaRequiredReaction(newVal) {
285-
elem.attr('aria-required', !!newVal);
286-
});
287-
}
288+
if (needsTabIndex) {
289+
elem.attr('tabindex', 0);
290+
}
288291

289-
if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem)) {
290-
scope.$watch(function ngAriaInvalidWatch() {
291-
return ngModel.$invalid;
292-
}, function ngAriaInvalidReaction(newVal) {
293-
elem.attr('aria-invalid', !!newVal);
294-
});
295-
}
292+
if (ngModel.$validators.required && shouldAttachAttr('aria-required', 'ariaRequired', elem)) {
293+
scope.$watch(function ngAriaRequiredWatch() {
294+
return ngModel.$error.required;
295+
}, function ngAriaRequiredReaction(newVal) {
296+
elem.attr('aria-required', !!newVal);
297+
});
298+
}
299+
300+
if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem)) {
301+
scope.$watch(function ngAriaInvalidWatch() {
302+
return ngModel.$invalid;
303+
}, function ngAriaInvalidReaction(newVal) {
304+
elem.attr('aria-invalid', !!newVal);
305+
});
306+
}
307+
}
308+
};
296309
}
297310
};
298311
}])

test/ngAria/ariaSpec.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,39 @@ describe('$aria', function() {
155155
});
156156

157157
it('should attach itself to role="radio"', function() {
158-
scope.$apply("val = 'one'");
159-
compileElement('<div role="radio" ng-model="val" value="{{val}}"></div>');
158+
scope.val = 'one';
159+
compileElement('<div role="radio" ng-model="val" value="one"></div>');
160160
expect(element.attr('aria-checked')).toBe('true');
161+
162+
scope.$apply("val = 'two'");
163+
expect(element.attr('aria-checked')).toBe('false');
161164
});
162165

163166
it('should attach itself to role="checkbox"', function() {
164167
scope.val = true;
165168
compileElement('<div role="checkbox" ng-model="val"></div>');
166169
expect(element.attr('aria-checked')).toBe('true');
170+
171+
scope.$apply('val = false');
172+
expect(element.attr('aria-checked')).toBe('false');
167173
});
168174

169175
it('should attach itself to role="menuitemradio"', function() {
170176
scope.val = 'one';
171-
compileElement('<div role="menuitemradio" ng-model="val" value="{{val}}"></div>');
177+
compileElement('<div role="menuitemradio" ng-model="val" value="one"></div>');
172178
expect(element.attr('aria-checked')).toBe('true');
179+
180+
scope.$apply("val = 'two'");
181+
expect(element.attr('aria-checked')).toBe('false');
173182
});
174183

175184
it('should attach itself to role="menuitemcheckbox"', function() {
176185
scope.val = true;
177186
compileElement('<div role="menuitemcheckbox" ng-model="val"></div>');
178187
expect(element.attr('aria-checked')).toBe('true');
188+
189+
scope.$apply('val = false');
190+
expect(element.attr('aria-checked')).toBe('false');
179191
});
180192

181193
it('should not attach itself if an aria-checked value is already present', function() {

0 commit comments

Comments
 (0)