Skip to content

Commit ad28baa

Browse files
committed
refactor(ngAria): bind to ngModel rather than form types
1 parent 8f9c4da commit ad28baa

File tree

2 files changed

+118
-151
lines changed

2 files changed

+118
-151
lines changed

src/ngAria/aria.js

+109-141
Original file line numberDiff line numberDiff line change
@@ -81,52 +81,27 @@ function $AriaProvider() {
8181
config = angular.extend(config, newConfig);
8282
};
8383

84-
function dashCase(input) {
85-
return input.replace(/[A-Z]/g, function(letter, pos) {
86-
return (pos ? '-' : '') + letter.toLowerCase();
84+
function camelCase(input) {
85+
return input.replace(/-./g, function(letter, pos) {
86+
return letter[1].toUpperCase();
8787
});
8888
}
8989

90-
function watchAttr(attrName, ariaName) {
91-
var ariaDashName = dashCase(ariaName);
92-
return function(scope, elem, attr) {
93-
if (!config[ariaName] || elem.attr(ariaDashName)) {
94-
return;
95-
}
96-
var destroyWatcher = attr.$observe(attrName, function(newVal) {
97-
elem.attr(ariaDashName, !angular.isUndefined(newVal));
98-
});
99-
scope.$on('$destroy', destroyWatcher);
100-
};
101-
}
10290

103-
function watchExpr(attrName, ariaName, negate) {
104-
var ariaDashName = dashCase(ariaName);
91+
function watchExpr(attrName, ariaAttr, negate) {
92+
var ariaCamelName = camelCase(ariaAttr);
10593
return function(scope, elem, attr) {
106-
if (config[ariaName] && !attr[ariaName]) {
94+
if (config[ariaCamelName] && !attr[ariaCamelName]) {
10795
scope.$watch(attr[attrName], function(boolVal) {
10896
if (negate) {
10997
boolVal = !boolVal;
11098
}
111-
elem.attr(ariaDashName, boolVal);
99+
elem.attr(ariaAttr, boolVal);
112100
});
113101
}
114102
};
115103
}
116104

117-
function watchNgModelProperty (prop, watchFn) {
118-
var ariaAttrName = 'aria-' + prop,
119-
configName = 'aria' + prop[0].toUpperCase() + prop.substr(1);
120-
return function watchNgModelPropertyLinkFn(scope, elem, attr, ngModel) {
121-
if (!config[configName] || elem.attr(ariaAttrName) || !ngModel) {
122-
return;
123-
}
124-
scope.$watch(watchFn(ngModel), function(newVal) {
125-
elem.attr(ariaAttrName, !!newVal);
126-
});
127-
};
128-
}
129-
130105
/**
131106
* @ngdoc service
132107
* @name $aria
@@ -140,135 +115,128 @@ function $AriaProvider() {
140115
*/
141116
this.$get = function() {
142117
return {
143-
watchExpr: watchExpr,
144-
ariaChecked: watchExpr('ngModel', 'ariaChecked'),
145-
ariaDisabled: watchExpr('ngDisabled', 'ariaDisabled'),
146-
ariaRequired: watchNgModelProperty('required', function(ngModel) {
147-
return function ngAriaModelWatch() {
148-
return ngModel.$error.required;
149-
};
150-
}),
151-
ariaInvalid: watchNgModelProperty('invalid', function(ngModel) {
152-
return function ngAriaModelWatch() {
153-
return ngModel.$invalid;
154-
};
155-
}),
156-
ariaValue: function(scope, elem, attr, ngModel) {
157-
if (config.ariaValue) {
158-
if (attr.min && !elem.attr('aria-valuemin')) {
159-
elem.attr('aria-valuemin', attr.min);
160-
}
161-
if (attr.max && !elem.attr('aria-valuemax')) {
162-
elem.attr('aria-valuemax', attr.max);
163-
}
164-
if (ngModel && !elem.attr('aria-valuenow')) {
165-
scope.$watch(function ngAriaModelWatch() {
166-
return ngModel.$modelValue;
167-
}, function ngAriaValueNowReaction(newVal) {
168-
elem.attr('aria-valuenow', newVal);
169-
});
170-
}
171-
}
172-
},
173-
radio: function(scope, elem, attr, ngModel) {
174-
if (config.ariaChecked && ngModel && !elem.attr('aria-checked')) {
175-
var needsTabIndex = config.tabindex && !elem.attr('tabindex');
176-
scope.$watch(function() {
177-
return ngModel.$modelValue;
178-
}, function(newVal) {
179-
elem.attr('aria-checked', newVal === attr.value);
180-
if (needsTabIndex) {
181-
elem.attr('tabindex', 0 - (newVal !== attr.value));
182-
}
183-
});
184-
}
185-
},
186-
multiline: function(scope, elem, attr) {
187-
if (config.ariaMultiline && !elem.attr('aria-multiline')) {
188-
elem.attr('aria-multiline', true);
189-
}
118+
config: function (key) {
119+
return config[camelCase(key)];
190120
},
191-
roleChecked: function(scope, elem, attr) {
192-
if (config.ariaChecked && attr.checked && !elem.attr('aria-checked')) {
193-
elem.attr('aria-checked', true);
194-
}
195-
},
196-
tabindex: function(scope, elem, attr) {
197-
if (config.tabindex && !elem.attr('tabindex')) {
198-
elem.attr('tabindex', 0);
199-
}
200-
}
121+
$$watchExpr: watchExpr
201122
};
202123
};
203124
}
204125

205-
var ngAriaRequired = ['$aria', function($aria) {
206-
return {
207-
require: '?ngModel',
208-
link: $aria.ariaRequired
209-
};
210-
}];
211-
212126
var ngAriaTabindex = ['$aria', function($aria) {
213-
return $aria.tabindex;
127+
return function(scope, elem, attr) {
128+
if ($aria.config('tabindex') && !elem.attr('tabindex')) {
129+
elem.attr('tabindex', 0);
130+
}
131+
};
214132
}];
215133

216134
ngAriaModule.directive('ngShow', ['$aria', function($aria) {
217-
return $aria.watchExpr('ngShow', 'ariaHidden', true);
135+
return $aria.$$watchExpr('ngShow', 'aria-hidden', true);
218136
}])
219137
.directive('ngHide', ['$aria', function($aria) {
220-
return $aria.watchExpr('ngHide', 'ariaHidden', false);
138+
return $aria.$$watchExpr('ngHide', 'aria-hidden', false);
221139
}])
222-
.directive('input', ['$aria', function($aria) {
140+
.directive('ngModel', ['$aria', function($aria) {
141+
142+
function shouldAttachAttr (attr, elem) {
143+
return $aria.config(attr) && !elem.attr(attr);
144+
}
145+
146+
function getShape (attr, elem) {
147+
var type = attr.type,
148+
role = attr.role;
149+
150+
return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' :
151+
((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' :
152+
(type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' :
153+
(type || role) === 'textbox' || elem[0].nodeName === 'TEXTAREA' ? 'multiline' : '';
154+
}
155+
223156
return {
224-
restrict: 'E',
157+
restrict: 'A',
225158
require: '?ngModel',
226159
link: function(scope, elem, attr, ngModel) {
227-
if (attr.type === 'checkbox') {
228-
$aria.ariaChecked(scope, elem, attr);
229-
} else if (attr.type === 'radio') {
230-
$aria.radio(scope, elem, attr, ngModel);
231-
} else if (attr.type === 'range') {
232-
$aria.ariaValue(scope, elem, attr, ngModel);
160+
var shape = getShape(attr, elem);
161+
var needsTabIndex = shouldAttachAttr('tabindex', elem);
162+
163+
function ngAriaWatchModelValue() {
164+
return ngModel.$modelValue;
165+
}
166+
167+
function getRadioReaction() {
168+
if (needsTabIndex) {
169+
needsTabIndex = false;
170+
return function ngAriaRadioReaction(newVal) {
171+
var boolVal = newVal === attr.value;
172+
elem.attr('aria-checked', boolVal);
173+
elem.attr('tabindex', 0 - !boolVal);
174+
};
175+
} else {
176+
return function ngAriaRadioReaction(newVal) {
177+
elem.attr('aria-checked', newVal === attr.value);
178+
};
179+
}
180+
}
181+
182+
function ngAriaCheckboxReaction(newVal) {
183+
elem.attr('aria-checked', !!newVal);
184+
}
185+
186+
switch (shape) {
187+
case 'radio':
188+
case 'checkbox':
189+
if (shouldAttachAttr('aria-checked', elem)) {
190+
scope.$watch(ngAriaWatchModelValue, shape === 'radio' ?
191+
getRadioReaction() : ngAriaCheckboxReaction);
192+
}
193+
break;
194+
case 'range':
195+
if ($aria.config('ariaValue')) {
196+
if (attr.min && !elem.attr('aria-valuemin')) {
197+
elem.attr('aria-valuemin', attr.min);
198+
}
199+
if (attr.max && !elem.attr('aria-valuemax')) {
200+
elem.attr('aria-valuemax', attr.max);
201+
}
202+
if (!elem.attr('aria-valuenow')) {
203+
scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) {
204+
elem.attr('aria-valuenow', newVal);
205+
});
206+
}
207+
}
208+
break;
209+
case 'multiline':
210+
if (shouldAttachAttr('aria-multiline', elem)) {
211+
elem.attr('aria-multiline', true);
212+
}
213+
break;
214+
}
215+
216+
if (needsTabIndex) {
217+
elem.attr('tabindex', 0);
218+
}
219+
220+
if (ngModel.$validators.required && shouldAttachAttr('aria-required', elem)) {
221+
scope.$watch(function ngAriaRequiredWatch() {
222+
return ngModel.$error.required;
223+
}, function ngAriaRequiredReaction(newVal) {
224+
elem.attr('aria-required', !!newVal);
225+
});
226+
}
227+
228+
if (shouldAttachAttr('aria-invalid', elem)) {
229+
scope.$watch(function ngAriaInvalidWatch() {
230+
return ngModel.$invalid;
231+
}, function ngAriaInvalidReaction(newVal) {
232+
elem.attr('aria-invalid', !!newVal);
233+
});
233234
}
234-
$aria.ariaInvalid(scope, elem, attr, ngModel);
235-
}
236-
};
237-
}])
238-
.directive('textarea', ['$aria', function($aria) {
239-
return {
240-
restrict: 'E',
241-
require: '?ngModel',
242-
link: function(scope, elem, attr, ngModel) {
243-
$aria.ariaInvalid(scope, elem, attr, ngModel);
244-
$aria.multiline(scope, elem, attr);
245235
}
246236
};
247237
}])
248-
.directive('ngRequired', ngAriaRequired)
249-
.directive('required', ngAriaRequired)
250238
.directive('ngDisabled', ['$aria', function($aria) {
251-
return $aria.ariaDisabled;
252-
}])
253-
.directive('role', ['$aria', function($aria) {
254-
return {
255-
restrict: 'A',
256-
require: '?ngModel',
257-
link: function(scope, elem, attr, ngModel) {
258-
if (attr.role === 'textbox') {
259-
$aria.multiline(scope, elem, attr);
260-
} else if (attr.role === 'progressbar' || attr.role === 'slider') {
261-
$aria.ariaValue(scope, elem, attr, ngModel);
262-
} else if (attr.role === 'checkbox' || attr.role === 'menuitemcheckbox') {
263-
$aria.roleChecked(scope, elem, attr);
264-
$aria.tabindex(scope, elem, attr);
265-
} else if (attr.role === 'radio' || attr.role === 'menuitemradio') {
266-
$aria.radio(scope, elem, attr, ngModel);
267-
} else if (attr.role === 'button') {
268-
$aria.tabindex(scope, elem, attr);
269-
}
270-
}
271-
};
239+
return $aria.$$watchExpr('ngDisabled', 'aria-disabled');
272240
}])
273241
.directive('ngClick', ngAriaTabindex)
274242
.directive('ngDblclick', ngAriaTabindex);

test/ngAria/ariaSpec.js

+9-10
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,20 @@ describe('$aria', function() {
105105
});
106106

107107
it('should attach itself to role="checkbox"', function() {
108-
compileInput('<div role="checkbox" checked="checked"></div>');
108+
scope.val = true;
109+
compileInput('<div role="checkbox" ng-model="val"></div>');
109110
expect(element.attr('aria-checked')).toBe('true');
110111
});
111112

112113
it('should attach itself to role="menuitemradio"', function() {
113-
scope.$apply("val = 'one'");
114+
scope.val = 'one';
114115
compileInput('<div role="menuitemradio" ng-model="val" value="{{val}}"></div>');
115116
expect(element.attr('aria-checked')).toBe('true');
116117
});
117118

118119
it('should attach itself to role="menuitemcheckbox"', function() {
119-
compileInput('<div role="menuitemcheckbox" checked="checked"></div>');
120+
scope.val = true;
121+
compileInput('<div role="menuitemcheckbox" ng-model="val"></div>');
120122
expect(element.attr('aria-checked')).toBe('true');
121123
});
122124

@@ -330,12 +332,12 @@ describe('$aria', function() {
330332
beforeEach(injectScopeAndCompiler);
331333

332334
it('should attach itself to textarea', function() {
333-
compileInput('<textarea></textarea>');
335+
compileInput('<textarea ng-model="val"></textarea>');
334336
expect(element.attr('aria-multiline')).toBe('true');
335337
});
336338

337339
it('should attach itself role="textbox"', function() {
338-
compileInput('<div role="textbox"></div>');
340+
compileInput('<div role="textbox" ng-model="val"></div>');
339341
expect(element.attr('aria-multiline')).toBe('true');
340342
});
341343

@@ -423,11 +425,8 @@ describe('$aria', function() {
423425
describe('tabindex', function() {
424426
beforeEach(injectScopeAndCompiler);
425427

426-
it('should attach tabindex to role=button, role=checkbox, ng-click and ng-dblclick', function() {
427-
compileInput('<div role="button"></div>');
428-
expect(element.attr('tabindex')).toBe('0');
429-
430-
compileInput('<div role="checkbox"></div>');
428+
it('should attach tabindex to role="checkbox", ng-click, and ng-dblclick', function() {
429+
compileInput('<div role="checkbox" ng-model="val"></div>');
431430
expect(element.attr('tabindex')).toBe('0');
432431

433432
compileInput('<div ng-click="someAction()"></div>');

0 commit comments

Comments
 (0)