Skip to content

Commit 087948b

Browse files
committed
Merge pull request #191 from rzajac/keyboard-support
Add keyboard support [WIP]
2 parents 06c0822 + 99829da commit 087948b

File tree

10 files changed

+447
-78
lines changed

10 files changed

+447
-78
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules/
22
.idea/
33
bower_components/
4-
temp/
4+
temp/
5+
tests/coverage/

Gruntfile.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
module.exports = function(grunt) {
1+
module.exports = function (grunt) {
22
// Project configuration.
33
grunt.initConfig({
44

55
pkg: grunt.file.readJSON('package.json'),
66

77
minBanner: '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
8-
'(c) <%= pkg.author %>, <%= pkg.repository.url %> - ' +
9-
'<%= grunt.template.today("yyyy-mm-dd") %> */\n',
8+
'(c) <%= pkg.author %>, <%= pkg.repository.url %> - ' +
9+
'<%= grunt.template.today("yyyy-mm-dd") %> */\n',
1010

1111
recess: {
1212
options: {
@@ -58,10 +58,10 @@ module.exports = function(grunt) {
5858
removeStyleLinkTypeAttributes: true
5959
},
6060
module: 'rzModule',
61-
url: function(url) {
61+
url: function (url) {
6262
return url.replace('src/', '');
6363
},
64-
bootstrap: function(module, script) {
64+
bootstrap: function (module, script) {
6565
return 'module.run(function($templateCache) {\n' + script + '\n});';
6666
}
6767
}
@@ -118,8 +118,13 @@ module.exports = function(grunt) {
118118
options: {
119119
port: 9000
120120
}
121+
},
122+
karma: {
123+
unit: {
124+
configFile: 'karma.conf.js',
125+
singleRun: true
126+
}
121127
}
122-
123128
});
124129

125130
grunt.loadNpmTasks('grunt-contrib-uglify');
@@ -129,8 +134,10 @@ module.exports = function(grunt) {
129134
grunt.loadNpmTasks('grunt-ng-annotate');
130135
grunt.loadNpmTasks('grunt-contrib-watch');
131136
grunt.loadNpmTasks('grunt-serve');
137+
grunt.loadNpmTasks('grunt-karma');
132138

133139
grunt.registerTask('default', ['css', 'js']);
140+
grunt.registerTask('test', ['karma']);
134141

135142
grunt.registerTask('css', ['recess']);
136143
grunt.registerTask('js', ['ngtemplates', 'replace', 'ngAnnotate', 'uglify']);

bower.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
"Jussi Saarivirta <[email protected]>"
99
],
1010
"description": "AngularJS slider directive with no external dependencies. Mobile friendly!",
11-
"main": ["dist/rzslider.js", "dist/rzslider.css"],
11+
"main": [
12+
"dist/rzslider.js",
13+
"dist/rzslider.css"
14+
],
1215
"keywords": [
1316
"angularjs",
1417
"slider"

dist/rzslider.js

Lines changed: 141 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
ticksValuesTooltip: null,
5252
vertical: false,
5353
selectionBarColor: null,
54+
keyboardSupport: true,
5455
scale: 1,
5556
onStart: null,
5657
onChange: null,
@@ -281,7 +282,6 @@
281282
this.initElemHandles();
282283
this.manageElementsStyle();
283284
this.addAccessibility();
284-
this.manageEventsBindings();
285285
this.setDisabledState();
286286
this.calcViewDimensions();
287287
this.setMinAndMax();
@@ -290,7 +290,7 @@
290290
self.updateCeilLab();
291291
self.updateFloorLab();
292292
self.initHandles();
293-
self.bindEvents();
293+
self.manageEventsBindings();
294294
});
295295

296296
// Recalculate slider view dimensions
@@ -311,6 +311,7 @@
311311
self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
312312
self.updateSelectionBar();
313313
self.updateTicksScale();
314+
self.updateAriaAttributes();
314315

315316
if (self.range) {
316317
self.updateCmbLabel();
@@ -324,6 +325,7 @@
324325
self.updateSelectionBar();
325326
self.updateTicksScale();
326327
self.updateCmbLabel();
328+
self.updateAriaAttributes();
327329
}, self.options.interval);
328330

329331
this.scope.$on('rzSliderForceRender', function() {
@@ -615,14 +617,47 @@
615617
},
616618

617619
/**
618-
* Adds accessibility atributes
620+
* Adds accessibility attributes
619621
*
620622
* Run only once during initialization
621623
*
622624
* @returns {undefined}
623625
*/
624626
addAccessibility: function() {
625-
this.sliderElem.attr("role", "slider");
627+
this.minH.attr('role', 'slider');
628+
this.updateAriaAttributes();
629+
if (this.options.keyboardSupport)
630+
this.minH.attr('tabindex', '0');
631+
if (this.options.vertical)
632+
this.minH.attr('aria-orientation', 'vertical');
633+
634+
if (this.range) {
635+
this.maxH.attr('role', 'slider');
636+
if (this.options.keyboardSupport)
637+
this.maxH.attr('tabindex', '0');
638+
if (this.options.vertical)
639+
this.maxH.attr('aria-orientation', 'vertical');
640+
}
641+
},
642+
643+
/**
644+
* Updates aria attributes according to current values
645+
*/
646+
updateAriaAttributes: function() {
647+
this.minH.attr({
648+
'aria-valuenow': this.scope.rzSliderModel,
649+
'aria-valuetext': this.customTrFn(this.scope.rzSliderModel),
650+
'aria-valuemin': this.minValue,
651+
'aria-valuemax': this.maxValue
652+
});
653+
if (this.range) {
654+
this.maxH.attr({
655+
'aria-valuenow': this.scope.rzSliderHigh,
656+
'aria-valuetext': this.customTrFn(this.scope.rzSliderHigh),
657+
'aria-valuemin': this.minValue,
658+
'aria-valuemax': this.maxValue
659+
});
660+
}
626661
},
627662

628663
/**
@@ -1014,16 +1049,16 @@
10141049
* @returns {number}
10151050
*/
10161051
valueToOffset: function(val) {
1017-
return (this.sanitizeOffsetValue(val) - this.minValue) * this.maxPos / this.valueRange || 0;
1052+
return (this.sanitizeValue(val) - this.minValue) * this.maxPos / this.valueRange || 0;
10181053
},
10191054

10201055
/**
1021-
* Ensure that the position rendered is within the slider bounds, even if the value is not
1056+
* Returns a value that is within slider range
10221057
*
10231058
* @param {number} val
10241059
* @returns {number}
10251060
*/
1026-
sanitizeOffsetValue: function(val) {
1061+
sanitizeValue: function(val) {
10271062
return Math.min(Math.max(val, this.minValue), this.maxValue);
10281063
},
10291064

@@ -1086,6 +1121,16 @@
10861121
return Math.abs(offset - this.minH.rzsp) < Math.abs(offset - this.maxH.rzsp) ? this.minH : this.maxH;
10871122
},
10881123

1124+
/**
1125+
* Wrapper function to focus an angular element
1126+
*
1127+
* @param el {AngularElement} the element to focus
1128+
*/
1129+
focusElement: function(el) {
1130+
var DOM_ELEMENT = 0;
1131+
el[DOM_ELEMENT].focus();
1132+
},
1133+
10891134
/**
10901135
* Bind mouse and touch events to slider handles
10911136
*
@@ -1126,6 +1171,13 @@
11261171
this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar));
11271172
this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null));
11281173
this.ticks.on('touchstart', angular.bind(this, this.onMove, this.ticks));
1174+
1175+
if (this.options.keyboardSupport) {
1176+
this.minH.on('focus', angular.bind(this, this.onPointerFocus, this.minH, 'rzSliderModel'));
1177+
if (this.range) {
1178+
this.maxH.on('focus', angular.bind(this, this.onPointerFocus, this.maxH, 'rzSliderHigh'));
1179+
}
1180+
}
11291181
},
11301182

11311183
/**
@@ -1156,10 +1208,6 @@
11561208
event.stopPropagation();
11571209
event.preventDefault();
11581210

1159-
if (this.tracking !== '') {
1160-
return;
1161-
}
1162-
11631211
// We have to do this in case the HTML where the sliders are on
11641212
// have been animated into view.
11651213
this.calcViewDimensions();
@@ -1173,6 +1221,9 @@
11731221

11741222
pointer.addClass('rz-active');
11751223

1224+
if (this.options.keyboardSupport)
1225+
this.focusElement(pointer);
1226+
11761227
ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer);
11771228
ehEnd = angular.bind(this, this.onEnd, ehMove);
11781229

@@ -1210,6 +1261,74 @@
12101261
this.positionTrackingHandle(newValue, newOffset);
12111262
},
12121263

1264+
/**
1265+
* onEnd event handler
1266+
*
1267+
* @param {Event} event The event
1268+
* @param {Function} ehMove The the bound move event handler
1269+
* @returns {undefined}
1270+
*/
1271+
onEnd: function(ehMove, event) {
1272+
var moveEventName = this.getEventNames(event).moveEvent;
1273+
1274+
if (!this.options.keyboardSupport) {
1275+
this.minH.removeClass('rz-active');
1276+
this.maxH.removeClass('rz-active');
1277+
this.tracking = '';
1278+
}
1279+
this.dragging.active = false;
1280+
1281+
$document.off(moveEventName, ehMove);
1282+
this.scope.$emit('slideEnded');
1283+
this.callOnEnd();
1284+
},
1285+
1286+
onPointerFocus: function(pointer, ref) {
1287+
this.tracking = ref;
1288+
pointer.one('blur', angular.bind(this, this.onPointerBlur, pointer));
1289+
pointer.on('keydown', angular.bind(this, this.onKeyboardEvent));
1290+
pointer.addClass('rz-active');
1291+
},
1292+
1293+
onPointerBlur: function(pointer) {
1294+
pointer.off('keydown');
1295+
this.tracking = '';
1296+
pointer.removeClass('rz-active');
1297+
},
1298+
1299+
onKeyboardEvent: function(event) {
1300+
var currentValue = this.scope[this.tracking],
1301+
keyCode = event.keyCode || event.which,
1302+
keys = {
1303+
38: 'UP',
1304+
40: 'DOWN',
1305+
37: 'LEFT',
1306+
39: 'RIGHT',
1307+
33: 'PAGEUP',
1308+
34: 'PAGEDOWN',
1309+
36: 'HOME',
1310+
35: 'END'
1311+
},
1312+
actions = {
1313+
UP: currentValue + this.step,
1314+
DOWN: currentValue - this.step,
1315+
LEFT: currentValue - this.step,
1316+
RIGHT: currentValue + this.step,
1317+
PAGEUP: currentValue + this.valueRange / 10,
1318+
PAGEDOWN: currentValue - this.valueRange / 10,
1319+
HOME: this.minValue,
1320+
END: this.maxValue
1321+
},
1322+
key = keys[keyCode],
1323+
action = actions[key];
1324+
if (action == null || this.tracking === '') return;
1325+
event.preventDefault();
1326+
1327+
var newValue = this.roundStep(this.sanitizeValue(action)),
1328+
newOffset = this.valueToOffset(newValue);
1329+
this.positionTrackingHandle(newValue, newOffset);
1330+
},
1331+
12131332
/**
12141333
* onDragStart event handler
12151334
*
@@ -1302,58 +1421,47 @@
13021421
*/
13031422
positionTrackingHandle: function(newValue, newOffset) {
13041423
var valueChanged = false;
1424+
var switched = false;
13051425

13061426
if (this.range) {
13071427
/* This is to check if we need to switch the min and max handles*/
13081428
if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh) {
1429+
switched = true;
13091430
this.scope[this.tracking] = this.scope.rzSliderHigh;
13101431
this.updateHandles(this.tracking, this.maxH.rzsp);
1432+
this.updateAriaAttributes();
13111433
this.tracking = 'rzSliderHigh';
13121434
this.minH.removeClass('rz-active');
13131435
this.maxH.addClass('rz-active');
1436+
if (this.options.keyboardSupport)
1437+
this.focusElement(this.maxH);
13141438
valueChanged = true;
13151439
} else if (this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel) {
1440+
switched = true;
13161441
this.scope[this.tracking] = this.scope.rzSliderModel;
13171442
this.updateHandles(this.tracking, this.minH.rzsp);
1443+
this.updateAriaAttributes();
13181444
this.tracking = 'rzSliderModel';
13191445
this.maxH.removeClass('rz-active');
13201446
this.minH.addClass('rz-active');
1447+
if (this.options.keyboardSupport)
1448+
this.focusElement(this.minH);
13211449
valueChanged = true;
13221450
}
13231451
}
13241452

13251453
if (this.scope[this.tracking] !== newValue) {
13261454
this.scope[this.tracking] = newValue;
13271455
this.updateHandles(this.tracking, newOffset);
1456+
this.updateAriaAttributes();
13281457
valueChanged = true;
13291458
}
13301459

13311460
if (valueChanged) {
13321461
this.scope.$apply();
13331462
this.callOnChange();
13341463
}
1335-
},
1336-
1337-
/**
1338-
* onEnd event handler
1339-
*
1340-
* @param {Event} event The event
1341-
* @param {Function} ehMove The the bound move event handler
1342-
* @returns {undefined}
1343-
*/
1344-
onEnd: function(ehMove, event) {
1345-
var moveEventName = this.getEventNames(event).moveEvent;
1346-
1347-
this.minH.removeClass('rz-active');
1348-
this.maxH.removeClass('rz-active');
1349-
1350-
$document.off(moveEventName, ehMove);
1351-
1352-
this.scope.$emit('slideEnded');
1353-
this.tracking = '';
1354-
1355-
this.dragging.active = false;
1356-
this.callOnEnd();
1464+
return switched;
13571465
},
13581466

13591467
/**

dist/rzslider.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/rzslider.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)