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

Commit

Permalink
feat(rating): make widget accessible
Browse files Browse the repository at this point in the history
 * 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
  • Loading branch information
bekos authored and pkozlowski-opensource committed Jan 31, 2014
1 parent 4a9dbbe commit 4f56e60
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 13 deletions.
23 changes: 13 additions & 10 deletions src/rating/rating.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,38 +23,41 @@ angular.module('ui.bootstrap.rating', [])
};

this.buildTemplateObjects = function(states) {
var defaultOptions = {
stateOn: this.stateOn,
stateOff: this.stateOff
};

for (var i = 0, n = states.length; i < n; i++) {
states[i] = angular.extend({ index: i }, defaultOptions, states[i]);
states[i] = angular.extend({ index: i }, { stateOn: this.stateOn, stateOff: this.stateOff }, states[i]);
}
return states;
};

$scope.rate = function(value) {
if ( !$scope.readonly ) {
if ( !$scope.readonly && value >= 0 && value <= $scope.range.length ) {
ngModelCtrl.$setViewValue(value);
ngModelCtrl.$render();
}
};

$scope.enter = function(value) {
if ( !$scope.readonly ) {
$scope.val = value;
$scope.value = value;
}
$scope.onHover({value: value});
};

$scope.reset = function() {
$scope.val = ngModelCtrl.$viewValue;
$scope.value = ngModelCtrl.$viewValue;
$scope.onLeave();
};

$scope.onKeydown = function(evt) {
if (/(37|38|39|40)/.test(evt.which)) {
evt.preventDefault();
evt.stopPropagation();
$scope.rate( $scope.value + (evt.which === 38 || evt.which === 39 ? 1 : -1) );
}
};

this.render = function() {
$scope.val = ngModelCtrl.$viewValue;
$scope.value = ngModelCtrl.$viewValue;
};
}])

Expand Down
53 changes: 52 additions & 1 deletion src/rating/test/rating.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,34 @@ describe('rating directive', function () {
return state;
}

function triggerKeyDown(keyCode) {
var e = $.Event('keydown');
e.which = keyCode;
element.trigger(e);
}

it('contains the default number of icons', function() {
expect(getStars().length).toBe(5);
expect(element.attr('aria-valuemax')).toBe('5');
});

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

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

getStar(5).click();
$rootScope.$digest();
expect(getState()).toEqual([true, true, true, true, true]);
expect($rootScope.rate).toBe(5);
expect(element.attr('aria-valuenow')).toBe('5');
});

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

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

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

expect(getStars().length).toBe(7);
expect(element.attr('aria-valuemax')).toBe('7');
});

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

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

describe('keyboard navigation', function() {
it('supports arrow keys', function() {
triggerKeyDown(38);
expect($rootScope.rate).toBe(4);

triggerKeyDown(37);
expect($rootScope.rate).toBe(3);
triggerKeyDown(40);
expect($rootScope.rate).toBe(2);

triggerKeyDown(39);
expect($rootScope.rate).toBe(3);
});

it('can get zero value but not negative', function() {
$rootScope.rate = 1;
$rootScope.$digest();

triggerKeyDown(37);
expect($rootScope.rate).toBe(0);

triggerKeyDown(37);
expect($rootScope.rate).toBe(0);
});

it('cannot get value above max', function() {
$rootScope.rate = 4;
$rootScope.$digest();

triggerKeyDown(38);
expect($rootScope.rate).toBe(5);

triggerKeyDown(38);
expect($rootScope.rate).toBe(5);
});
});

describe('custom states', function() {
beforeEach(inject(function() {
$rootScope.classOn = 'icon-ok-sign';
Expand All @@ -150,7 +200,8 @@ describe('rating directive', function () {
}));

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

it('handles each icon', function() {
Expand Down
6 changes: 4 additions & 2 deletions template/rating/rating.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<span ng-mouseleave="reset()">
<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>
<span ng-mouseleave="reset()" ng-keydown="onKeydown($event)" tabindex="0" role="slider" aria-valuemin="0" aria-valuemax="{{range.length}}" aria-valuenow="{{value}}">
<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')">
<span class="sr-only">({{ $index < value ? '*' : ' ' }})</span>
</i>
</span>

0 comments on commit 4f56e60

Please sign in to comment.