diff --git a/Gruntfile.js b/Gruntfile.js index 017dab71..f22c0d23 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -15,6 +15,7 @@ module.exports = function(grunt) { 'src/js/scope_start.js', 'src/js/structure.js', 'src/js/tooltips.js', + 'src/js/aria.js', 'src/js/pips.js', 'src/js/scope_helpers.js', 'src/js/scope_events.js', diff --git a/concat.php b/concat.php index 531c8922..8bbd2a4f 100644 --- a/concat.php +++ b/concat.php @@ -10,6 +10,7 @@ require 'src/js/scope_start.js'; require 'src/js/structure.js'; require 'src/js/tooltips.js'; + require 'src/js/aria.js'; require 'src/js/pips.js'; require 'src/js/scope_helpers.js'; require 'src/js/scope_events.js'; diff --git a/src/js/aria.js b/src/js/aria.js new file mode 100644 index 00000000..8639dd96 --- /dev/null +++ b/src/js/aria.js @@ -0,0 +1,23 @@ + + function aria ( ) { + + bindEvent('update', function ( values, handleNumber, unencoded, tap, positions ) { + + // Update Aria Values for all handles, as a change in one changes min and max values for the next. + scope_HandleNumbers.forEach(function( handleNumber ){ + + var handle = scope_Handles[handleNumber]; + + var min = checkHandlePosition(scope_Locations, handleNumber, 0, true, true, true); + var max = checkHandlePosition(scope_Locations, handleNumber, 100, true, true, true); + + var now = positions[handleNumber]; + var text = options.ariaFormat.to(unencoded[handleNumber]); + + handle.children[0].setAttribute('aria-valuemin', min.toFixed(1)); + handle.children[0].setAttribute('aria-valuemax', max.toFixed(1)); + handle.children[0].setAttribute('aria-valuenow', now.toFixed(1)); + handle.children[0].setAttribute('aria-valuetext', text); + }); + }); + } diff --git a/src/js/helpers.js b/src/js/helpers.js index 19917bea..667261ec 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -1,4 +1,8 @@ + function isValidFormatter ( entry ) { + return typeof entry === 'object' && typeof entry.to === 'function' && typeof entry.from === 'function'; + } + function removeElement ( el ) { el.parentElement.removeChild(el); } diff --git a/src/js/options.js b/src/js/options.js index 8de17cd4..34da1d67 100644 --- a/src/js/options.js +++ b/src/js/options.js @@ -15,6 +15,16 @@ return value !== undefined && value.toFixed(2); }, 'from': Number }; + function validateFormat ( entry ) { + + // Any object with a to and from method is supported. + if ( isValidFormatter(entry) ) { + return true; + } + + throw new Error("noUiSlider (" + VERSION + "): 'format' requires 'to' and 'from' methods."); + } + function testStep ( parsed, entry ) { if ( !isNumeric( entry ) ) { @@ -283,16 +293,14 @@ } } - function testFormat ( parsed, entry ) { + function testAriaFormat ( parsed, entry ) { + parsed.ariaFormat = entry; + validateFormat(entry); + } + function testFormat ( parsed, entry ) { parsed.format = entry; - - // Any object with a to and from method is supported. - if ( typeof entry.to === 'function' && typeof entry.from === 'function' ) { - return true; - } - - throw new Error("noUiSlider (" + VERSION + "): 'format' requires 'to' and 'from' methods."); + validateFormat(entry); } function testCssPrefix ( parsed, entry ) { @@ -344,6 +352,7 @@ padding: 0, animate: true, animationDuration: 300, + ariaFormat: defaultFormatter, format: defaultFormatter }; @@ -362,6 +371,7 @@ 'limit': { r: false, t: testLimit }, 'padding': { r: false, t: testPadding }, 'behaviour': { r: true, t: testBehaviour }, + 'ariaFormat': { r: false, t: testAriaFormat }, 'format': { r: false, t: testFormat }, 'tooltips': { r: false, t: testTooltips }, 'cssPrefix': { r: false, t: testCssPrefix }, @@ -412,6 +422,11 @@ 'useRequestAnimationFrame': true }; + // AriaFormat defaults to regular format, if any. + if ( options.format && !options.ariaFormat ) { + options.ariaFormat = options.format; + } + // Run all options through a testing mechanism to ensure correct // input. It should be noted that options might get modified to // be handled properly. E.g. wrapping integers in arrays. diff --git a/src/js/scope.js b/src/js/scope.js index e0880203..07baf389 100644 --- a/src/js/scope.js +++ b/src/js/scope.js @@ -1,6 +1,6 @@ // Split out the handle positioning logic so the Move event can use it, too - function checkHandlePosition ( reference, handleNumber, to, lookBackward, lookForward ) { + function checkHandlePosition ( reference, handleNumber, to, lookBackward, lookForward, getValue ) { // For sliders with multiple handles, limit movement to the other handle. // Apply the margin option by adding it to the handle positions. @@ -48,7 +48,7 @@ to = limit(to); // Return false if handle can't move - if ( to === reference[handleNumber] ) { + if ( to === reference[handleNumber] && !getValue ) { return false; } @@ -101,7 +101,7 @@ // Test suggested values and apply margin, step. function setHandle ( handleNumber, to, lookBackward, lookForward ) { - to = checkHandlePosition(scope_Locations, handleNumber, to, lookBackward, lookForward); + to = checkHandlePosition(scope_Locations, handleNumber, to, lookBackward, lookForward, false); if ( to === false ) { return false; @@ -398,4 +398,6 @@ tooltips(); } + aria(); + return scope_Self; diff --git a/src/js/scope_helpers.js b/src/js/scope_helpers.js index f42dfdbf..e62a3ac8 100644 --- a/src/js/scope_helpers.js +++ b/src/js/scope_helpers.js @@ -167,7 +167,7 @@ handleNumbers.forEach(function(handleNumber, o) { - var to = checkHandlePosition(proposals, handleNumber, proposals[handleNumber] + proposal, b[o], f[o]); + var to = checkHandlePosition(proposals, handleNumber, proposals[handleNumber] + proposal, b[o], f[o], false); // Stop if one of the handles can't move. if ( to === false ) { diff --git a/src/js/structure.js b/src/js/structure.js index d7456c38..92892e37 100644 --- a/src/js/structure.js +++ b/src/js/structure.js @@ -7,6 +7,12 @@ handle.setAttribute('data-handle', handleNumber); + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + // 0 = focusable and reachable + handle.setAttribute('tabindex', '0'); + handle.setAttribute('role', 'slider'); + handle.setAttribute('aria-orientation', options.ort ? 'vertical' : 'horizontal'); + if ( handleNumber === 0 ) { addClass(handle, options.cssClasses.handleLower); }