From 7779b0f010b72806ba1968b1514bea222cb52889 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 8 Sep 2014 06:49:32 -0400 Subject: [PATCH] feat(slider): add full-featured slider component Closes #260, #245, #221, #160, #36, #31, #21, #255. --- docs/templates/demo/template.index.html | 1 - src/base/_mixins.scss | 5 + src/base/utils.js | 31 +++ src/components/slider/_slider.scss | 336 ++++++++++++++--------- src/components/slider/demo1/index.html | 59 ++++- src/components/slider/demo1/script.js | 14 +- src/components/slider/slider.js | 339 +++++++++++++++++++----- src/components/slider/slider.spec.js | 91 ++++++- src/theme/_variables.scss | 22 +- 9 files changed, 691 insertions(+), 207 deletions(-) diff --git a/docs/templates/demo/template.index.html b/docs/templates/demo/template.index.html index aa7e53ffd89..7e0dbc846ba 100644 --- a/docs/templates/demo/template.index.html +++ b/docs/templates/demo/template.index.html @@ -2,7 +2,6 @@ - diff --git a/src/base/_mixins.scss b/src/base/_mixins.scss index 1345fce5f0b..aeb1059f8f6 100644 --- a/src/base/_mixins.scss +++ b/src/base/_mixins.scss @@ -59,6 +59,11 @@ transition-delay: $val; } +@mixin animation($val) { + -webkit-animation: $val; + animation: $val; +} + @mixin user-select($val:none) { -webkit-user-select: $val; -moz-user-select: $val; diff --git a/src/base/utils.js b/src/base/utils.js index f1c61a562e5..212bf55c4a1 100644 --- a/src/base/utils.js +++ b/src/base/utils.js @@ -105,7 +105,38 @@ var Util = { return target; } + }, + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + debounce: function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + clearTimeout(timeout); + timeout = setTimeout(function() { + timeout = null; + if (!immediate) func.apply(context, args); + }, wait); + if (immediate && !timeout) func.apply(context, args); + }; } + }; +/* + * Since removing jQuery from the demos, some code that uses `element.focus()` is broken. + * + * We need to add `element.focus()`, because it's testable unlike `element[0].focus`. + * + * TODO(ajoslin): This should be added in a better place later. + */ +angular.element.prototype.focus = angular.element.prototype.focus || function() { + if (this.length) { + this[0].focus(); + } + return this; +}; diff --git a/src/components/slider/_slider.scss b/src/components/slider/_slider.scss index fd47c1d8c7d..aa8e530982b 100644 --- a/src/components/slider/_slider.scss +++ b/src/components/slider/_slider.scss @@ -1,158 +1,258 @@ +@include keyframes(sliderFocusThumb) { + 0% { + opacity: 0; + @include transform(scale(0.0)); + } + 50% { + @include transform(scale(1.0)); + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@mixin slider-thumb-position($width: $slider-thumb-width, $height: $slider-thumb-height) { + position: absolute; + left: -$width / 2; + top: ($slider-height / 2) - ($height / 2); + width: $width; + height: $height; + border-radius: max($width, $height); +} + material-slider { - &:before { - display: table; - content: ' '; - } + height: $slider-height; + position: relative; + display: block; + margin-left: 4px; + margin-right: 4px; - // Track - .material-track { - position: relative; - margin: $slider-height; + /** + * Track + */ + .slider-track-container { + width: 100%; + position: absolute; + top: ($slider-height / 2) - ($slider-track-height) / 2; + height: $slider-track-height; + } + .slider-track { + position: absolute; + left: 0; + right: 0; + height: 100%; + background: #C8C8C8; + } + .slider-track-fill { + background: #03A9F4; + @include transition(width 0.05s linear); + } + .slider-track-ticks { + position: absolute; + left: 0; + right: 0; + height: 100%; } - .material-fill, - .material-tick-markers { + /** + * Slider thumb + */ + .slider-thumb-container { position: absolute; - top: 13px; left: 0; - height: $slider-track-height; - background: $theme-light-blue; - pointer-events: none; + top: 0; + @include transform(translateX(0)); + transition: transform 0.1s linear; + -webkit-transition: -webkit-transform 0.1s linear; } + .slider-thumb { + z-index: 1; - .material-tick-markers { - left: -1px; - z-index: 0; - width: 100%; - background: transparent; - visibility: invisible; + // Positioning the outer area of the thumb 6px bigger than it needs to be keeps + // the :after area being clipped by the background of the focus-thumb animation. + @include slider-thumb-position($slider-thumb-width + 6, $slider-thumb-height + 6); - &.visible { - visibility: visible; + // We render thumb in an :after selector to fix an obscure problem with the + // thumb being clipped by the focus-ring and focus-thumb while running the focus + // animation. + &:after { + content: ''; + position: absolute; + left: 3px; + top: 3px; + width: $slider-thumb-width; + height: $slider-thumb-height; + border-radius: max($slider-thumb-width, $slider-thumb-height); + border: 3px solid $theme-light-blue; + background-color: $theme-light-blue; } + @include transform(scale($slider-thumb-default-scale)); + @include transition(all 0.1s linear); + } + + /* The sign that's focused in discrete mode */ + .slider-sign { + + /* Center the children (slider-thumb-text) */ @include flex-display(); + @include flex-align-items(center); + @include flex-justify-content(center); - .material-tick, - &:after { - margin-top: 1px; - height: 16px; - border-left: 2px solid black; - @include flex(1); - } + position: absolute; + left: -($slider-sign-height / 2); + top: $slider-sign-top; + width: $slider-sign-width; + height: $slider-sign-height; + border-radius: max($slider-sign-height, $slider-sign-width); + background-color: $theme-light-blue; + + @include transform(scale(0.4) translateY((-$slider-sign-top + 8) / 0.4)); + @include transition(all 0.2s ease-in-out); + + /* The arrow pointing down under the sign */ &:after { position: absolute; - right: -2px; content: ''; - } - } + left: -($slider-sign-width / 2 - $slider-arrow-width / 2); + border-radius: $slider-arrow-height; + top: 19px; + border-left: $slider-arrow-width / 2 solid transparent; + border-right: $slider-arrow-width / 2 solid transparent; + border-top: $slider-arrow-height solid $theme-light-blue; - // Simulated Slider Thumb - // the native thumb is hidden in place of this one - .material-thumb { - position: absolute; - top: (-$slider-thumb-height / 2) + 1; - right: -$slider-thumb-width / 2; - z-index: 1; - display: table; - width: $slider-thumb-width; - height: $slider-thumb-height; - border: 2px solid $theme-light-blue; - border-radius: max($slider-thumb-height, $slider-thumb-width); - background: $theme-light-blue; - -webkit-transition: all .1s ease-in-out; - -moz-transition: all .1s ease-in-out; - transition: all .1s ease-in-out; - } - - // Active Simulate Slider Thumb - .material-active .material-thumb { - border: 2px solid $theme-light-blue; - -webkit-transform: scale(2); - -moz-transform: scale(2); - transform: scale(2); - } - - &.material-slider-min { - input:active + .material-fill .material-thumb { - border-width: 1px; + opacity: 0; + @include transform(translateY(-8px)); + @include transition(all 0.2s ease-in-out); } - .material-thumb { - background: #fff; + + .slider-thumb-text { + z-index: 1; + color: white; + font-size: 12px; + font-weight: bold; } } - input { - margin: 0; - padding: 0; - width: 100%; - height: $slider-track-height; - outline: none; - border: none; - background: $slider-background-color; - cursor: pointer; - -webkit-appearance: none; - -webkit-tap-highlight-color: rgba(255, 255, 255, 0); + /** + * The border/background that comes in when focused in non-discrete mode + */ + .slider-focus-thumb { + @include slider-thumb-position($slider-focus-thumb-width, $slider-focus-thumb-height); + display: none; + opacity: 0; + background-color: #C0C0C0; + @include animation(sliderFocusThumb 0.4s linear); } - - input::-moz-range-track { - border: inherit; - background: transparent; + .slider-focus-ring { + @include slider-thumb-position($slider-focus-thumb-width, $slider-focus-thumb-height); + border: 2px solid #D6D6D6; + background-color: transparent; + @include transform(scale(0)); + @include transition(all 0.2s linear); } - - input::-ms-track { - border: inherit; - background: transparent; - color: transparent; + .slider-disabled-thumb { + @include slider-thumb-position( + $slider-thumb-width + $slider-thumb-disabled-border * 2, + $slider-thumb-height + $slider-thumb-disabled-border * 2 + ); + @include transform(scale($slider-thumb-disabled-scale)); + border: $slider-thumb-disabled-border solid white; + display: none; } - input::-ms-fill-lower, - input::-ms-fill-upper { - background: transparent; + &.slider-min { + .slider-thumb { + &:after { + background-color: white; + } + } + .slider-sign { + opacity: 0; + } } - input::-ms-tooltip { - display: none; + &:focus { + outline: none; } - // Native Range Thumb - // the native thumb is actually transparent - // but much larger to create a bigger hit area - input::-webkit-slider-thumb { - position: relative; - width: $slider-thumb-width * 2; - height: $slider-thumb-height * 4; - border: none; - background: transparent; - -webkit-appearance: none; - } - input::-webkit-slider-thumb:before { - position: absolute; - left: -$slider-thumb-width * 2; - display: table; - width: $slider-thumb-width * 6; - height: $slider-thumb-height * 4; - content: ' '; + /* Don't animate left/right while panning */ + &.panning { + .slider-thumb-container, + .slider-track-fill { + @include transition(none); + } } - input::-moz-range-thumb { - width: $slider-thumb-width * 2; - height: $slider-thumb-height * 4; - border: none; - background: transparent; + &:not([discrete]) { + /* Hide the sign and ticks in non-discrete mode */ + .slider-track-ticks, + .slider-sign { + display: none; + } + + &:not([disabled]) { + &:hover { + .slider-thumb { + @include transform(scale($slider-thumb-hover-scale)); + } + } + + &:focus, + &.active { + .slider-focus-thumb { + display: block; + } + .slider-focus-ring { + @include transform(scale(1)); + } + .slider-thumb { + @include transform(scale($slider-thumb-focus-scale)); + } + } + } } - input::-moz-focus-outer { - border: 0; + &[discrete] { + /* Hide the focus thumb in discrete mode */ + .slider-focus-thumb, + .slider-focus-ring { + display: none; + } + + &:not([disabled]) { + &:focus, + &.active { + .slider-sign, + .slider-sign:after { + opacity: 1; + @include transform(translateY(0) scale(1.0)); + } + } + } } - input::-ms-thumb { - width: $slider-thumb-width * 2; - height: $slider-thumb-height * 4; - border: 0; - background: transparent; + &[disabled] { + .slider-track-fill { + display: none; + } + .slider-thumb { + @include transform(scale($slider-thumb-disabled-scale)); + &:after { + border-color: #BEBEBE; + } + } + &:not(.slider-min) .slider-thumb { + &:after { + background-color: #BEBEBE; + } + } + .slider-disabled-thumb { + display: block; + } } } diff --git a/src/components/slider/demo1/index.html b/src/components/slider/demo1/index.html index 95a73bb164b..df110724966 100644 --- a/src/components/slider/demo1/index.html +++ b/src/components/slider/demo1/index.html @@ -1,20 +1,57 @@
- - - +

+ RGB       +

- - - +
+
+ R +
+ + +
+ +
+
+ +
+
+ G +
+ + +
+ +
+
+ +
+
+ B +
+ + +
+ +
+
- - +

Rating: {{rating}}/5

+ -

Slider 1: {{ data.slider1 }}

-

Slider 2: {{ data.slider2 }}

-

Slider 3: {{ data.slider3 }}

+

Disabled

+ + +

Disabled, Discrete

+ +
+ diff --git a/src/components/slider/demo1/script.js b/src/components/slider/demo1/script.js index 371773d60f6..c7dd535d929 100644 --- a/src/components/slider/demo1/script.js +++ b/src/components/slider/demo1/script.js @@ -3,10 +3,14 @@ angular.module('app', ['ngMaterial']) .controller('AppCtrl', function($scope) { - $scope.data = { - slider1: 0, - slider2: 50, - slider3: 8, - } + $scope.color = { + red: Math.floor(Math.random() * 255), + green: Math.floor(Math.random() * 255), + blue: Math.floor(Math.random() * 255) + }; + + $scope.rating = 3; + $scope.disabled1 = 0; + $scope.disabled2 = 70; }); diff --git a/src/components/slider/slider.js b/src/components/slider/slider.js index 5c273c96530..4f6e2e6d3f3 100644 --- a/src/components/slider/slider.js +++ b/src/components/slider/slider.js @@ -1,112 +1,315 @@ /** * @ngdoc module * @name material.components.slider - * @description Slider module! */ -angular.module('material.components.slider', []) - .directive('materialSlider', [ - '$window', - materialSliderDirective - ]); +angular.module('material.components.slider', [ + 'material.animations', + 'material.services.aria' +]) +.directive('materialSlider', [ + SliderDirective +]); /** * @ngdoc directive * @name materialSlider * @module material.components.slider * @restrict E - * * @description - * The `material-slider` directive creates a slider bar that you can use. + * The `` component allows the user to choose from a range of + * values. * - * Simply put a native `` element inside of a - * `` container. + * It has two modes: 'normal' mode, where the user slides between a wide range + * of values, and 'discrete' mode, where the user slides between only a few + * select values. * - * On the range input, all HTML5 range attributes are supported. + * To enable discrete mode, add the `discrete` attribute to a slider, + * and use the `step` attribute to change the distance between + * values the user is allowed to pick. * * @usage + *

Normal Mode

+ * + * + * + * + *

Discrete Mode

* - * - * + * * * + * + * @param {boolean=} discrete Whether to enable discrete mode. + * @param {number=} step The distance between values the user is allowed to pick. Default 1. + * @param {number=} min The minimum value the user is allowed to pick. Default 0. + * @param {number=} max The maximum value the user is allowed to pick. Default 100. */ -function materialSliderDirective($window) { - - var MIN_VALUE_CSS = 'material-slider-min'; - var ACTIVE_CSS = 'material-active'; +function SliderDirective() { + return { + scope: {}, + require: ['?ngModel', 'materialSlider'], + controller: [ + '$scope', + '$element', + '$attrs', + '$$rAF', + '$timeout', + '$window', + '$materialEffects', + '$aria', + SliderController + ], + template: + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
', + link: postLink + }; - function rangeSettings(rangeEle) { - return { - min: parseInt( rangeEle.min !== "" ? rangeEle.min : 0, 10 ), - max: parseInt( rangeEle.max !== "" ? rangeEle.max : 100, 10 ), - step: parseInt( rangeEle.step !== "" ? rangeEle.step : 1, 10 ) + function postLink(scope, element, attr, ctrls) { + var ngModelCtrl = ctrls[0] || { + // Mock ngModelController if it doesn't exist to give us + // the minimum functionality needed + $setViewValue: function(val) { + this.$viewValue = val; + this.$viewChangeListeners.forEach(function(cb) { cb(); }); + }, + $parsers: [], + $formatters: [], + $viewChangeListeners: [] }; + + var sliderCtrl = ctrls[1]; + sliderCtrl.init(ngModelCtrl); } +} - return { - restrict: 'E', - scope: true, - transclude: true, - template: '
', - link: link - }; +/** + * We use a controller for all the logic so that we can expose a few + * things to unit tests + */ +function SliderController(scope, element, attr, $$rAF, $timeout, $window, $materialEffects, $aria) { + + this.init = function init(ngModelCtrl) { + var thumb = angular.element(element[0].querySelector('.slider-thumb')); + var thumbContainer = thumb.parent(); + var trackContainer = angular.element(element[0].querySelector('.slider-track-container')); + var activeTrack = angular.element(element[0].querySelector('.slider-track-fill')); + var tickContainer = angular.element(element[0].querySelector('.slider-track-ticks')); - // ********************************************************** - // Private Methods - // ********************************************************** + // Default values, overridable by attrs + attr.min ? attr.$observe('min', updateMin) : updateMin(0); + attr.max ? attr.$observe('max', updateMax) : updateMax(100); + attr.step ? attr.$observe('step', updateStep) : updateStep(1); - function link(scope, element, attr) { - var input = element.find('input'); - var ngModelCtrl = angular.element(input).controller('ngModel'); + attr.ngDisabled ? + scope.$watch(attr.ngDisabled, updateAriaDisabled) : + updateAriaDisabled(!!attr.disabled); - if(!input || !ngModelCtrl || input[0].type !== 'range') return; + $aria.expect(element, 'aria-label'); + element.attr('tabIndex', 0); + element.attr('role', Constant.ARIA.ROLE.SLIDER); + element.on('keydown', keydownListener); - var rangeEle = input[0]; - var trackEle = angular.element( element[0].querySelector('.material-track') ); + var hammertime = new Hammer(element[0], { + recognizers: [ + [Hammer.Pan, { direction: Hammer.DIRECTION_HORIZONTAL }] + ] + }); + hammertime.on('hammer.input', onInput); + hammertime.on('panstart', onPanStart); + hammertime.on('pan', onPan); - trackEle.append('
'); - var fillEle = trackEle[0].querySelector('.material-fill'); + // On resize, recalculate the slider's dimensions and re-render + var onWindowResize = $$rAF.debounce(function() { + refreshSliderDimensions(); + ngModelRender(); + redrawTicks(); + }, false); + angular.element($window).on('resize', onWindowResize); - if(input.attr('step')) { - var settings = rangeSettings(rangeEle); - var tickCount = (settings.max - settings.min) / settings.step; - var tickMarkersEle = angular.element('
'); - for(var i=0; i'); + scope.$on('$destroy', function() { + angular.element($window).off('resize', onWindowResize); + hammertime.destroy(); + }); + + ngModelCtrl.$render = ngModelRender; + ngModelCtrl.$viewChangeListeners.push(ngModelRender); + ngModelCtrl.$formatters.push(minMaxValidator); + ngModelCtrl.$formatters.push(stepValidator); + + /** + * Attributes + */ + var min; + var max; + var step; + function updateMin(value) { + min = parseFloat(value); + element.attr('aria-valuemin', value); + } + function updateMax(value) { + max = parseFloat(value); + element.attr('aria-valuemax', value); + } + function updateStep(value) { + step = parseFloat(value); + redrawTicks(); + } + function updateAriaDisabled(isDisabled) { + element.attr('aria-disabled', !!isDisabled); + } + + // Draw the ticks with canvas. + // The alternative to drawing ticks with canvas is to draw one element for each tick, + // which could quickly become a performance bottleneck. + var tickCanvas, tickCtx; + function redrawTicks() { + if (!angular.isDefined(attr.discrete)) return; + + var numSteps = Math.floor( (max - min) / step ); + if (!tickCanvas) { + tickCanvas = angular.element(''); + tickCtx = tickCanvas[0].getContext('2d'); + tickCtx.fillStyle = 'black'; + tickContainer.append(tickCanvas); } - if (tickCount > 0) { - tickMarkersEle.addClass('visible'); + var dimensions = getSliderDimensions(); + tickCanvas[0].width = dimensions.width; + tickCanvas[0].height = dimensions.height; + + var distance; + for (var i = 0; i <= numSteps; i++) { + distance = Math.floor(dimensions.width * (i / numSteps)); + tickCtx.fillRect(distance - 1, 0, 2, dimensions.height); } - trackEle.append(tickMarkersEle); } - input.on('mousedown touchstart', function(e){ - trackEle.addClass(ACTIVE_CSS); - }); - - input.on('mouseup touchend', function(e){ - trackEle.removeClass(ACTIVE_CSS); - }); + /** + * Refreshing Dimensions + */ + var sliderDimensions = {}; + var debouncedRefreshDimensions = Util.debounce(refreshSliderDimensions, 5000); + refreshSliderDimensions(); + function refreshSliderDimensions() { + sliderDimensions = trackContainer[0].getBoundingClientRect(); + } + function getSliderDimensions() { + debouncedRefreshDimensions(); + return sliderDimensions; + } - function render() { - var settings = rangeSettings(rangeEle); - var adjustedValue = parseInt(ngModelCtrl.$viewValue, 10) - settings.min; - var fillRatio = (adjustedValue / (settings.max - settings.min)); + /** + * left/right arrow listener + */ + function keydownListener(ev) { + var changeAmount; + if (ev.which === Constant.KEY_CODE.LEFT_ARROW) { + changeAmount = -step; + } else if (ev.which === Constant.KEY_CODE.RIGHT_ARROW) { + changeAmount = step; + } + if (changeAmount) { + if (ev.metaKey || ev.ctrlKey || ev.altKey) { + changeAmount *= 4; + } + ev.preventDefault(); + ev.stopPropagation(); + scope.$evalAsync(function() { + setModelValue(ngModelCtrl.$viewValue + changeAmount); + }); + } + } - fillEle.style.width = (fillRatio * 100) + '%'; + /** + * ngModel setters and validators + */ + function setModelValue(value) { + ngModelCtrl.$setViewValue( minMaxValidator(stepValidator(value)) ); + } + function ngModelRender() { + var percent = (ngModelCtrl.$viewValue - min) / (max - min); + scope.modelValue = ngModelCtrl.$viewValue; + element.attr('aria-valuenow', ngModelCtrl.$viewValue); + setSliderPercent(percent); + } - if(fillRatio <= 0) { - element.addClass(MIN_VALUE_CSS); - } else { - element.removeClass(MIN_VALUE_CSS); + function minMaxValidator(value) { + if (angular.isNumber(value)) { + return Math.max(min, Math.min(max, value)); } + } + function stepValidator(value) { + if (angular.isNumber(value)) { + return Math.round(value / step) * step; + } + } + /** + * @param percent 0-1 + */ + function setSliderPercent(percent) { + activeTrack.css('width', (percent * 100) + '%'); + thumbContainer.css( + $materialEffects.TRANSFORM, + 'translateX(' + getSliderDimensions().width * percent + 'px)' + ); + element.toggleClass('slider-min', percent === 0); } - scope.$watch( function () { return ngModelCtrl.$viewValue; }, render ); - } + /** + * Slide listeners + */ + var isSliding = false; + function onInput(ev) { + if (!isSliding && ev.eventType === Hammer.INPUT_START && + !element[0].hasAttribute('disabled')) { -} + isSliding = true; + element.addClass('active'); + element[0].focus(); + refreshSliderDimensions(); + doSlide(ev.center.x); + + } else if (isSliding && ev.eventType === Hammer.INPUT_END) { + isSliding = false; + element.removeClass('panning active'); + } + } + function onPanStart() { + if (!isSliding) return; + element.addClass('panning'); + } + function onPan(ev) { + if (!isSliding) return; + doSlide(ev.center.x); + ev.preventDefault(); + } + + /** + * Expose for testing + */ + this._onInput = onInput; + this._onPanStart = onPanStart; + this._onPan = onPan; + + function doSlide(x) { + var percent = (x - sliderDimensions.left) / (sliderDimensions.width); + scope.$evalAsync(function() { setModelValue(min + percent * (max - min)); }); + } + }; +} diff --git a/src/components/slider/slider.spec.js b/src/components/slider/slider.spec.js index 61971f8426e..6cf1f245aa7 100644 --- a/src/components/slider/slider.spec.js +++ b/src/components/slider/slider.spec.js @@ -1,7 +1,94 @@ -describe('materialSlider', function() { +describe('material-slider', function() { - beforeEach(module('material.components.slider')); + beforeEach(module('material.components.slider','material.decorators')); + it('should set model on press', inject(function($compile, $rootScope, $timeout) { + var slider = $compile('')($rootScope); + $rootScope.$apply('value = 50'); + var sliderCtrl = slider.controller('materialSlider'); + + spyOn(slider.find('.slider-track-container')[0], 'getBoundingClientRect').andReturn({ + width: 100, + left: 0, + right: 0 + }); + + sliderCtrl._onInput({ + eventType: Hammer.INPUT_START, + center: { x: 30 } + }); + $timeout.flush(); + expect($rootScope.value).toBe(30); + + //When going past max, it should clamp to max + sliderCtrl._onPan({ + center: { x: 500 }, + preventDefault: angular.noop + }); + $timeout.flush(); + expect($rootScope.value).toBe(100); + + sliderCtrl._onPan({ + center: { x: 50 }, + preventDefault: angular.noop + }); + $timeout.flush(); + expect($rootScope.value).toBe(50); + })); + + it('should increment model on right arrow', inject(function($compile, $rootScope, $timeout) { + var slider = $compile( + '' + )($rootScope); + + $rootScope.$apply('model = 100'); + + TestUtil.triggerEvent(slider, 'keydown', { + keyCode: Constant.KEY_CODE.RIGHT_ARROW + }); + $timeout.flush(); + expect($rootScope.model).toBe(102); + + TestUtil.triggerEvent(slider, 'keydown', { + keyCode: Constant.KEY_CODE.RIGHT_ARROW + }); + $timeout.flush(); + expect($rootScope.model).toBe(104); + + // Stays at max + TestUtil.triggerEvent(slider, 'keydown', { + keyCode: Constant.KEY_CODE.RIGHT_ARROW + }); + $timeout.flush(); + expect($rootScope.model).toBe(104); + })); + + it('should decrement model on left arrow', inject(function($compile, $rootScope, $timeout) { + var slider = $compile( + '' + )($rootScope); + + $rootScope.$apply('model = 104'); + + TestUtil.triggerEvent(slider, 'keydown', { + keyCode: Constant.KEY_CODE.LEFT_ARROW + }); + $timeout.flush(); + expect($rootScope.model).toBe(102); + + TestUtil.triggerEvent(slider, 'keydown', { + keyCode: Constant.KEY_CODE.LEFT_ARROW + }); + $timeout.flush(); + expect($rootScope.model).toBe(100); + + // Stays at min + TestUtil.triggerEvent(slider, 'keydown', { + keyCode: Constant.KEY_CODE.LEFT_ARROW + }); + $timeout.flush(); + expect($rootScope.model).toBe(100); + })); }); diff --git a/src/theme/_variables.scss b/src/theme/_variables.scss index 238b8c7b708..6129166fc65 100644 --- a/src/theme/_variables.scss +++ b/src/theme/_variables.scss @@ -145,11 +145,29 @@ $input-focused-border-color: $text-dark; //-------------------------------------------- $slider-background-color: rgb(200, 200, 200); -$slider-height: 30px; +$slider-height: 48px; + $slider-track-height: 2px; -$slider-thumb-width: 10px; +$slider-thumb-width: 32px; $slider-thumb-height: $slider-thumb-width; +$slider-thumb-default-scale: 0.5; +$slider-thumb-hover-scale: 0.6; +$slider-thumb-focus-scale: 0.85; +$slider-thumb-disabled-scale: 0.35; +$slider-thumb-disabled-border: 6px; + +$slider-focus-thumb-width: 48px; +$slider-focus-thumb-height: $slider-focus-thumb-width; +$slider-focus-ring-border-width: 3px; + +$slider-arrow-height: 16px; +$slider-arrow-width: 28px; + +$slider-sign-height: 28px; +$slider-sign-width: 28px; +$slider-sign-top: ($slider-height / 2) - ($slider-thumb-default-scale * $slider-thumb-height / 2) - ($slider-sign-height) - ($slider-arrow-height) + 8px; + // Switch //--------------------------------------------