|
51 | 51 | ticksValuesTooltip: null,
|
52 | 52 | vertical: false,
|
53 | 53 | selectionBarColor: null,
|
| 54 | + keyboardSupport: true, |
54 | 55 | scale: 1,
|
55 | 56 | onStart: null,
|
56 | 57 | onChange: null,
|
|
281 | 282 | this.initElemHandles();
|
282 | 283 | this.manageElementsStyle();
|
283 | 284 | this.addAccessibility();
|
284 |
| - this.manageEventsBindings(); |
285 | 285 | this.setDisabledState();
|
286 | 286 | this.calcViewDimensions();
|
287 | 287 | this.setMinAndMax();
|
|
290 | 290 | self.updateCeilLab();
|
291 | 291 | self.updateFloorLab();
|
292 | 292 | self.initHandles();
|
293 |
| - self.bindEvents(); |
| 293 | + self.manageEventsBindings(); |
294 | 294 | });
|
295 | 295 |
|
296 | 296 | // Recalculate slider view dimensions
|
|
311 | 311 | self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel));
|
312 | 312 | self.updateSelectionBar();
|
313 | 313 | self.updateTicksScale();
|
| 314 | + self.updateAriaAttributes(); |
314 | 315 |
|
315 | 316 | if (self.range) {
|
316 | 317 | self.updateCmbLabel();
|
|
324 | 325 | self.updateSelectionBar();
|
325 | 326 | self.updateTicksScale();
|
326 | 327 | self.updateCmbLabel();
|
| 328 | + self.updateAriaAttributes(); |
327 | 329 | }, self.options.interval);
|
328 | 330 |
|
329 | 331 | this.scope.$on('rzSliderForceRender', function() {
|
|
615 | 617 | },
|
616 | 618 |
|
617 | 619 | /**
|
618 |
| - * Adds accessibility atributes |
| 620 | + * Adds accessibility attributes |
619 | 621 | *
|
620 | 622 | * Run only once during initialization
|
621 | 623 | *
|
622 | 624 | * @returns {undefined}
|
623 | 625 | */
|
624 | 626 | 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 | + } |
626 | 661 | },
|
627 | 662 |
|
628 | 663 | /**
|
|
1014 | 1049 | * @returns {number}
|
1015 | 1050 | */
|
1016 | 1051 | 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; |
1018 | 1053 | },
|
1019 | 1054 |
|
1020 | 1055 | /**
|
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 |
1022 | 1057 | *
|
1023 | 1058 | * @param {number} val
|
1024 | 1059 | * @returns {number}
|
1025 | 1060 | */
|
1026 |
| - sanitizeOffsetValue: function(val) { |
| 1061 | + sanitizeValue: function(val) { |
1027 | 1062 | return Math.min(Math.max(val, this.minValue), this.maxValue);
|
1028 | 1063 | },
|
1029 | 1064 |
|
|
1086 | 1121 | return Math.abs(offset - this.minH.rzsp) < Math.abs(offset - this.maxH.rzsp) ? this.minH : this.maxH;
|
1087 | 1122 | },
|
1088 | 1123 |
|
| 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 | + |
1089 | 1134 | /**
|
1090 | 1135 | * Bind mouse and touch events to slider handles
|
1091 | 1136 | *
|
|
1126 | 1171 | this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar));
|
1127 | 1172 | this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null));
|
1128 | 1173 | 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 | + } |
1129 | 1181 | },
|
1130 | 1182 |
|
1131 | 1183 | /**
|
|
1156 | 1208 | event.stopPropagation();
|
1157 | 1209 | event.preventDefault();
|
1158 | 1210 |
|
1159 |
| - if (this.tracking !== '') { |
1160 |
| - return; |
1161 |
| - } |
1162 |
| - |
1163 | 1211 | // We have to do this in case the HTML where the sliders are on
|
1164 | 1212 | // have been animated into view.
|
1165 | 1213 | this.calcViewDimensions();
|
|
1173 | 1221 |
|
1174 | 1222 | pointer.addClass('rz-active');
|
1175 | 1223 |
|
| 1224 | + if (this.options.keyboardSupport) |
| 1225 | + this.focusElement(pointer); |
| 1226 | + |
1176 | 1227 | ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer);
|
1177 | 1228 | ehEnd = angular.bind(this, this.onEnd, ehMove);
|
1178 | 1229 |
|
|
1210 | 1261 | this.positionTrackingHandle(newValue, newOffset);
|
1211 | 1262 | },
|
1212 | 1263 |
|
| 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 | + |
1213 | 1332 | /**
|
1214 | 1333 | * onDragStart event handler
|
1215 | 1334 | *
|
|
1302 | 1421 | */
|
1303 | 1422 | positionTrackingHandle: function(newValue, newOffset) {
|
1304 | 1423 | var valueChanged = false;
|
| 1424 | + var switched = false; |
1305 | 1425 |
|
1306 | 1426 | if (this.range) {
|
1307 | 1427 | /* This is to check if we need to switch the min and max handles*/
|
1308 | 1428 | if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh) {
|
| 1429 | + switched = true; |
1309 | 1430 | this.scope[this.tracking] = this.scope.rzSliderHigh;
|
1310 | 1431 | this.updateHandles(this.tracking, this.maxH.rzsp);
|
| 1432 | + this.updateAriaAttributes(); |
1311 | 1433 | this.tracking = 'rzSliderHigh';
|
1312 | 1434 | this.minH.removeClass('rz-active');
|
1313 | 1435 | this.maxH.addClass('rz-active');
|
| 1436 | + if (this.options.keyboardSupport) |
| 1437 | + this.focusElement(this.maxH); |
1314 | 1438 | valueChanged = true;
|
1315 | 1439 | } else if (this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel) {
|
| 1440 | + switched = true; |
1316 | 1441 | this.scope[this.tracking] = this.scope.rzSliderModel;
|
1317 | 1442 | this.updateHandles(this.tracking, this.minH.rzsp);
|
| 1443 | + this.updateAriaAttributes(); |
1318 | 1444 | this.tracking = 'rzSliderModel';
|
1319 | 1445 | this.maxH.removeClass('rz-active');
|
1320 | 1446 | this.minH.addClass('rz-active');
|
| 1447 | + if (this.options.keyboardSupport) |
| 1448 | + this.focusElement(this.minH); |
1321 | 1449 | valueChanged = true;
|
1322 | 1450 | }
|
1323 | 1451 | }
|
1324 | 1452 |
|
1325 | 1453 | if (this.scope[this.tracking] !== newValue) {
|
1326 | 1454 | this.scope[this.tracking] = newValue;
|
1327 | 1455 | this.updateHandles(this.tracking, newOffset);
|
| 1456 | + this.updateAriaAttributes(); |
1328 | 1457 | valueChanged = true;
|
1329 | 1458 | }
|
1330 | 1459 |
|
1331 | 1460 | if (valueChanged) {
|
1332 | 1461 | this.scope.$apply();
|
1333 | 1462 | this.callOnChange();
|
1334 | 1463 | }
|
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; |
1357 | 1465 | },
|
1358 | 1466 |
|
1359 | 1467 | /**
|
|
0 commit comments