Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit 4f56e60

Browse files
bekospkozlowski-opensource
authored andcommitted
feat(rating): make widget accessible
* Support keyboard navigation. * Add WAI-ARIA markup. * Text representation for screen readers. Source: http://mindtrove.info/creating-an-accessible-internationalized-dojo-rating-widget/ Closes #1707
1 parent 4a9dbbe commit 4f56e60

File tree

3 files changed

+69
-13
lines changed

3 files changed

+69
-13
lines changed

src/rating/rating.js

+13-10
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,41 @@ angular.module('ui.bootstrap.rating', [])
2323
};
2424

2525
this.buildTemplateObjects = function(states) {
26-
var defaultOptions = {
27-
stateOn: this.stateOn,
28-
stateOff: this.stateOff
29-
};
30-
3126
for (var i = 0, n = states.length; i < n; i++) {
32-
states[i] = angular.extend({ index: i }, defaultOptions, states[i]);
27+
states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
3328
}
3429
return states;
3530
};
3631

3732
$scope.rate = function(value) {
38-
if ( !$scope.readonly ) {
33+
if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
3934
ngModelCtrl.$setViewValue(value);
4035
ngModelCtrl.$render();
4136
}
4237
};
4338

4439
$scope.enter = function(value) {
4540
if ( !$scope.readonly ) {
46-
$scope.val = value;
41+
$scope.value = value;
4742
}
4843
$scope.onHover({value: value});
4944
};
5045

5146
$scope.reset = function() {
52-
$scope.val = ngModelCtrl.$viewValue;
47+
$scope.value = ngModelCtrl.$viewValue;
5348
$scope.onLeave();
5449
};
5550

51+
$scope.onKeydown = function(evt) {
52+
if (/(37|38|39|40)/.test(evt.which)) {
53+
evt.preventDefault();
54+
evt.stopPropagation();
55+
$scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
56+
}
57+
};
58+
5659
this.render = function() {
57-
$scope.val = ngModelCtrl.$viewValue;
60+
$scope.value = ngModelCtrl.$viewValue;
5861
};
5962
}])
6063

src/rating/test/rating.spec.js

+52-1
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,34 @@ describe('rating directive', function () {
2727
return state;
2828
}
2929

30+
function triggerKeyDown(keyCode) {
31+
var e = $.Event('keydown');
32+
e.which = keyCode;
33+
element.trigger(e);
34+
}
35+
3036
it('contains the default number of icons', function() {
3137
expect(getStars().length).toBe(5);
38+
expect(element.attr('aria-valuemax')).toBe('5');
3239
});
3340

3441
it('initializes the default star icons as selected', function() {
3542
expect(getState()).toEqual([true, true, true, false, false]);
43+
expect(element.attr('aria-valuenow')).toBe('3');
3644
});
3745

3846
it('handles correctly the click event', function() {
3947
getStar(2).click();
4048
$rootScope.$digest();
4149
expect(getState()).toEqual([true, true, false, false, false]);
4250
expect($rootScope.rate).toBe(2);
51+
expect(element.attr('aria-valuenow')).toBe('2');
4352

4453
getStar(5).click();
4554
$rootScope.$digest();
4655
expect(getState()).toEqual([true, true, true, true, true]);
4756
expect($rootScope.rate).toBe(5);
57+
expect(element.attr('aria-valuenow')).toBe('5');
4858
});
4959

5060
it('handles correctly the hover event', function() {
@@ -68,20 +78,23 @@ describe('rating directive', function () {
6878
$rootScope.$digest();
6979

7080
expect(getState()).toEqual([true, true, false, false, false]);
81+
expect(element.attr('aria-valuenow')).toBe('2');
7182
});
7283

7384
it('shows different number of icons when `max` attribute is set', function() {
7485
element = $compile('<rating ng-model="rate" max="7"></rating>')($rootScope);
7586
$rootScope.$digest();
7687

7788
expect(getStars().length).toBe(7);
89+
expect(element.attr('aria-valuemax')).toBe('7');
7890
});
7991

8092
it('shows different number of icons when `max` attribute is from scope variable', function() {
8193
$rootScope.max = 15;
8294
element = $compile('<rating ng-model="rate" max="max"></rating>')($rootScope);
8395
$rootScope.$digest();
8496
expect(getStars().length).toBe(15);
97+
expect(element.attr('aria-valuemax')).toBe('15');
8598
});
8699

87100
it('handles readonly attribute', function() {
@@ -124,6 +137,43 @@ describe('rating directive', function () {
124137
expect($rootScope.leaving).toHaveBeenCalled();
125138
});
126139

140+
describe('keyboard navigation', function() {
141+
it('supports arrow keys', function() {
142+
triggerKeyDown(38);
143+
expect($rootScope.rate).toBe(4);
144+
145+
triggerKeyDown(37);
146+
expect($rootScope.rate).toBe(3);
147+
triggerKeyDown(40);
148+
expect($rootScope.rate).toBe(2);
149+
150+
triggerKeyDown(39);
151+
expect($rootScope.rate).toBe(3);
152+
});
153+
154+
it('can get zero value but not negative', function() {
155+
$rootScope.rate = 1;
156+
$rootScope.$digest();
157+
158+
triggerKeyDown(37);
159+
expect($rootScope.rate).toBe(0);
160+
161+
triggerKeyDown(37);
162+
expect($rootScope.rate).toBe(0);
163+
});
164+
165+
it('cannot get value above max', function() {
166+
$rootScope.rate = 4;
167+
$rootScope.$digest();
168+
169+
triggerKeyDown(38);
170+
expect($rootScope.rate).toBe(5);
171+
172+
triggerKeyDown(38);
173+
expect($rootScope.rate).toBe(5);
174+
});
175+
});
176+
127177
describe('custom states', function() {
128178
beforeEach(inject(function() {
129179
$rootScope.classOn = 'icon-ok-sign';
@@ -150,7 +200,8 @@ describe('rating directive', function () {
150200
}));
151201

152202
it('should define number of icon elements', function () {
153-
expect(getStars().length).toBe($rootScope.states.length);
203+
expect(getStars().length).toBe(4);
204+
expect(element.attr('aria-valuemax')).toBe('4');
154205
});
155206

156207
it('handles each icon', function() {

template/rating/rating.html

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
<span ng-mouseleave="reset()">
2-
<i ng-repeat="r in range" ng-mouseenter="enter($index + 1)" ng-click="rate($index + 1)" class="glyphicon" ng-class="$index < val && (r.stateOn || 'glyphicon-star') || (r.stateOff || 'glyphicon-star-empty')"></i>
1+
<span ng-mouseleave="reset()" ng-keydown="onKeydown($event)" tabindex="0" role="slider" aria-valuemin="0" aria-valuemax="{{range.length}}" aria-valuenow="{{value}}">
2+
<i ng-repeat="r in range" ng-mouseenter="enter($index + 1)" ng-click="rate($index + 1)" class="glyphicon" ng-class="$index < value && (r.stateOn || 'glyphicon-star') || (r.stateOff || 'glyphicon-star-empty')">
3+
<span class="sr-only">({{ $index < value ? '*' : ' ' }})</span>
4+
</i>
35
</span>

0 commit comments

Comments
 (0)