From c176251b89efc521e80a21372bbfe53bfd426cc7 Mon Sep 17 00:00:00 2001 From: Elad Bezalel Date: Sun, 28 Feb 2016 01:39:35 +0200 Subject: [PATCH] feat(slider): vertical slider, UI fixes and bug fixes - Added vertical slider - Improved UI - Fixed dynamic min/max with values not in range - Added round attribute to set how many numbers should be after the dot, default is 3, maximum is 6 to prevent scientific notation. - Made input width grow or shrink according the text length - Added discrete readonly mode - Added disabled on slider container fixes #4371, #3259, #2421, #1221, #4576, #6996, #7093, #7093 closes #5874, #5872, #6051 --- .../slider/demoBasicUsage/index.html | 56 +-- .../slider/demoBasicUsage/script.js | 11 +- .../slider/demoBasicUsage/style.css | 5 - src/components/slider/demoVertical/index.html | 33 ++ src/components/slider/demoVertical/script.js | 9 + src/components/slider/slider-theme.scss | 112 +++++- src/components/slider/slider.js | 273 +++++++++++--- src/components/slider/slider.scss | 321 +++++++++++++---- src/components/slider/slider.spec.js | 334 ++++++++++++++++-- 9 files changed, 953 insertions(+), 201 deletions(-) delete mode 100644 src/components/slider/demoBasicUsage/style.css create mode 100644 src/components/slider/demoVertical/index.html create mode 100644 src/components/slider/demoVertical/script.js diff --git a/src/components/slider/demoBasicUsage/index.html b/src/components/slider/demoBasicUsage/index.html index 016dd4d13c4..20b98853d7c 100644 --- a/src/components/slider/demoBasicUsage/index.html +++ b/src/components/slider/demoBasicUsage/index.html @@ -6,38 +6,32 @@

RGB      

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

Rating: {{rating}}/5 - demo of theming classes

@@ -63,12 +57,20 @@

Rating: {{rating}}/5 - demo of theming classes

Disabled

- - + + + -

Disabled, Discrete

- - + + + +
+ Is disabled + +

Disabled, Discrete, Read Only

+ + + Read only diff --git a/src/components/slider/demoBasicUsage/script.js b/src/components/slider/demoBasicUsage/script.js index 12d55bd1358..24a5836493a 100644 --- a/src/components/slider/demoBasicUsage/script.js +++ b/src/components/slider/demoBasicUsage/script.js @@ -1,6 +1,9 @@ angular.module('sliderDemo1', ['ngMaterial']) - + .config(function($mdIconProvider) { + $mdIconProvider + .iconSet('device', 'img/icons/sets/device-icons.svg', 24); + }) .controller('AppCtrl', function($scope) { $scope.color = { @@ -13,7 +16,9 @@ angular.module('sliderDemo1', ['ngMaterial']) $scope.rating2 = 2; $scope.rating3 = 4; - $scope.disabled1 = 0; - $scope.disabled2 = 70; + $scope.disabled1 = Math.floor(Math.random() * 100); + $scope.disabled2 = 0; + $scope.disabled3 = 70; + $scope.isDisabled = true; }); diff --git a/src/components/slider/demoBasicUsage/style.css b/src/components/slider/demoBasicUsage/style.css deleted file mode 100644 index 43e7f809717..00000000000 --- a/src/components/slider/demoBasicUsage/style.css +++ /dev/null @@ -1,5 +0,0 @@ - -input[type="number"] { - text-align: center; - padding-left:10px; -} diff --git a/src/components/slider/demoVertical/index.html b/src/components/slider/demoVertical/index.html new file mode 100644 index 00000000000..87ea5f96f90 --- /dev/null +++ b/src/components/slider/demoVertical/index.html @@ -0,0 +1,33 @@ +
+ + + + + + + +
Volume
+
+ + + + + + +
Bass
+
+
+ + + + + +
Master
+
+ Read only +
+
+
diff --git a/src/components/slider/demoVertical/script.js b/src/components/slider/demoVertical/script.js new file mode 100644 index 00000000000..44290118682 --- /dev/null +++ b/src/components/slider/demoVertical/script.js @@ -0,0 +1,9 @@ + +angular.module('sliderDemo2', ['ngMaterial']) + +.controller('AppCtrl', function($scope) { + + $scope.vol = Math.floor(Math.random() * 100); + $scope.bass = Math.floor(Math.random() * 100); + $scope.master = Math.floor(Math.random() * 100); +}); diff --git a/src/components/slider/slider-theme.scss b/src/components/slider/slider-theme.scss index 05a36ac39fc..f4ae24fd993 100644 --- a/src/components/slider/slider-theme.scss +++ b/src/components/slider/slider-theme.scss @@ -4,21 +4,50 @@ md-slider.md-THEME_NAME-theme { background-color: '{{foreground-3}}'; } ._md-track-ticks { - background-color: '{{foreground-4}}'; - } - ._md-focus-thumb { - background-color: '{{foreground-2}}'; + color: '{{background-contrast}}'; } ._md-focus-ring { - background-color: '{{accent-color}}'; + background-color: '{{accent-200-0.38}}'; } ._md-disabled-thumb { border-color: '{{background-color}}'; - } - &._md-min ._md-thumb:after { background-color: '{{background-color}}'; } + &._md-min { + ._md-thumb:after { + background-color: '{{background-color}}'; + border-color: '{{foreground-3}}'; + } + + ._md-focus-ring { + background-color: '{{foreground-3-0.38}}'; + } + + &[md-discrete] { + ._md-thumb { + &:after { + background-color: '{{background-contrast}}'; + border-color: transparent; + } + } + + ._md-sign { + background-color: '{{background-400}}'; + &:after { + border-top-color: '{{background-400}}'; + } + } + + &[md-vertical] { + ._md-sign:after { + border-top-color: transparent; + border-left-color: '{{background-400}}'; + } + } + } + } + ._md-track._md-track-fill { background-color: '{{accent-color}}'; } @@ -32,13 +61,21 @@ md-slider.md-THEME_NAME-theme { border-top-color: '{{accent-color}}'; } } + + &[md-vertical] { + ._md-sign:after { + border-top-color: transparent; + border-left-color: '{{accent-color}}'; + } + } + ._md-thumb-text { color: '{{accent-contrast}}'; } &.md-warn { ._md-focus-ring { - background-color: '{{warn-color}}'; + background-color: '{{warn-200-0.38}}'; } ._md-track._md-track-fill { background-color: '{{warn-color}}'; @@ -54,6 +91,14 @@ md-slider.md-THEME_NAME-theme { border-top-color: '{{warn-color}}'; } } + + &[md-vertical] { + ._md-sign:after { + border-top-color: transparent; + border-left-color: '{{warn-color}}'; + } + } + ._md-thumb-text { color: '{{warn-contrast}}'; } @@ -61,7 +106,7 @@ md-slider.md-THEME_NAME-theme { &.md-primary { ._md-focus-ring { - background-color: '{{primary-color}}'; + background-color: '{{primary-200-0.38}}'; } ._md-track._md-track-fill { background-color: '{{primary-color}}'; @@ -77,6 +122,14 @@ md-slider.md-THEME_NAME-theme { border-top-color: '{{primary-color}}'; } } + + &[md-vertical] { + ._md-sign:after { + border-top-color: transparent; + border-left-color: '{{primary-color}}'; + } + } + ._md-thumb-text { color: '{{primary-contrast}}'; } @@ -84,10 +137,45 @@ md-slider.md-THEME_NAME-theme { &[disabled] { ._md-thumb:after { - border-color: '{{foreground-3}}'; + border-color: transparent; } - &:not(._md-min) ._md-thumb:after { - background-color: '{{foreground-3}}'; + &:not(._md-min), &[md-discrete] { + ._md-thumb:after { + background-color: '{{foreground-3}}'; + border-color: transparent; + } + } + } + + &[disabled][readonly] { + ._md-sign { + background-color: '{{background-400}}'; + &:after { + border-top-color: '{{background-400}}'; + } + } + + &[md-vertical] { + ._md-sign:after { + border-top-color: transparent; + border-left-color: '{{background-400}}'; + } + } + + ._md-disabled-thumb { + border-color: transparent; + background-color: transparent; } } } + +md-slider-container { + &[disabled] { + & > *:first-child, + & > *:last-child { + &:not(md-slider){ + color: '{{foreground-3}}'; + } + } + } +} \ No newline at end of file diff --git a/src/components/slider/slider.js b/src/components/slider/slider.js index 38eb51c57bd..af1d1da19a7 100644 --- a/src/components/slider/slider.js +++ b/src/components/slider/slider.js @@ -5,7 +5,84 @@ angular.module('material.components.slider', [ 'material.core' ]) - .directive('mdSlider', SliderDirective); + .directive('mdSlider', SliderDirective) + .directive('mdSliderContainer', SliderContainerDirective); + +/** + * @ngdoc directive + * @name mdSliderContainer + * @module material.components.slider + * @restrict E + * @description + * The `` contains slider with two other elements. + * + * + * @usage + *

Normal Mode

+ * + * + */ +function SliderContainerDirective() { + return { + controller: function () {}, + compile: function (elem) { + var slider = elem.find('md-slider'); + + if (!slider) { + return; + } + + var vertical = slider.attr('md-vertical'); + + if (vertical !== undefined) { + elem.attr('md-vertical', ''); + } + + if(!slider.attr('flex')) { + slider.attr('flex', ''); + } + + return function (scope, element, attr, ctrl) { + + // We have to manually stop the $watch on ngDisabled because it exists + // on the parent scope, and won't be automatically destroyed when + // the component is destroyed. + function setDisable(isDisabled) { + element.children().attr('disabled', isDisabled); + element.find('input').attr('disabled', isDisabled); + } + + var stopDisabledWatch = angular.noop; + if (attr.disabled) { + setDisable(true); + } + else if (attr.ngDisabled) { + stopDisabledWatch = scope.$watch(attr.ngDisabled, function (isDisabled) { + setDisable(isDisabled); + }); + } + + scope.$on('$destroy', function () { + stopDisabledWatch(); + }); + + var initialMaxWidth; + + ctrl.fitInputWidthToTextLength = function (length) { + var input = element.find('md-input-container'); + var computedStyle = getComputedStyle(input[0]); + var minWidth = parseInt(computedStyle['min-width']); + var padding = parseInt(computedStyle['padding']) * 2; + initialMaxWidth = initialMaxWidth || parseInt(computedStyle['max-width']); + + var newMaxWidth = Math.max(initialMaxWidth, minWidth + padding + (minWidth / 2 * length)); + + input.css('max-width', newMaxWidth + 'px'); + }; + } + } + } +} /** * @ngdoc directive @@ -44,26 +121,29 @@ * @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. + * @param {number=} round The amount of numbers after the decimal point, can't be above 6 to prevent scientific notation. Default 3. */ -function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log) { +function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdTheming, $mdGesture, $parse, $log, $timeout) { return { scope: {}, - require: '?ngModel', + require: ['?ngModel', '?^mdSliderContainer'], template: '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + '
' + - '
' + '
' + '
', compile: compile @@ -74,17 +154,23 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi // ********************************************************** function compile (tElement, tAttrs) { - if (!tAttrs.tabindex) tElement.attr('tabindex', 0); - tElement.attr('role', 'slider'); + var wrapper = angular.element(tElement[0].getElementsByClassName('_md-slider-wrapper')); + + var tabIndex = tAttrs.tabindex || 0; + wrapper.attr('tabindex', tabIndex); + + if (tAttrs.disabled || tAttrs.ngDisabled) wrapper.attr('tabindex', -1); + + wrapper.attr('role', 'slider'); $mdAria.expect(tElement, 'aria-label'); return postLink; } - function postLink(scope, element, attr, ngModelCtrl) { + function postLink(scope, element, attr, ctrls) { $mdTheming(element); - ngModelCtrl = ngModelCtrl || { + var ngModelCtrl = ctrls[0] || { // Mock ngModelController if it doesn't exist to give us // the minimum functionality needed $setViewValue: function(val) { @@ -96,10 +182,14 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi $viewChangeListeners: [] }; - var isDisabledGetter = angular.noop; - if (attr.disabled != null) { - isDisabledGetter = function() { return true; }; - } else if (attr.ngDisabled) { + var containerCtrl = ctrls[1]; + + var container = angular.element($mdUtil.getClosest(element, '_md-slider-container', true)); + + var isDisabledGetter = function () { + return element[0].hasAttribute('disabled'); + }; + if (attr.ngDisabled) { isDisabledGetter = angular.bind(null, $parse(attr.ngDisabled), scope.$parent); } @@ -109,12 +199,18 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi var trackContainer = angular.element(element[0].querySelector('._md-track-container')); var activeTrack = angular.element(element[0].querySelector('._md-track-fill')); var tickContainer = angular.element(element[0].querySelector('._md-track-ticks')); + var wrapper = angular.element(element[0].getElementsByClassName('_md-slider-wrapper')); + var content = angular.element(element[0].getElementsByClassName('_md-slider-content')); var throttledRefreshDimensions = $mdUtil.throttle(refreshSliderDimensions, 5000); // Default values, overridable by attrs + var DEFAULT_ROUND = 3; + var vertical = angular.isDefined(attr.mdVertical); + var discrete = angular.isDefined(attr.mdDiscrete); angular.isDefined(attr.min) ? attr.$observe('min', updateMin) : updateMin(0); angular.isDefined(attr.max) ? attr.$observe('max', updateMax) : updateMax(100); angular.isDefined(attr.step)? attr.$observe('step', updateStep) : updateStep(1); + angular.isDefined(attr.round)? attr.$observe('round', updateRound) : updateRound(DEFAULT_ROUND); // We have to manually stop the $watch on ngDisabled because it exists // on the parent scope, and won't be automatically destroyed when @@ -124,10 +220,15 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi stopDisabledWatch = scope.$parent.$watch(attr.ngDisabled, updateAriaDisabled); } - $mdGesture.register(element, 'drag'); + $mdGesture.register(wrapper, 'drag', { horizontal: !vertical }); + + scope.mouseActive = false; - element + wrapper .on('keydown', keydownListener) + .on('mousedown', mouseDownListener) + .on('focus', focusListener) + .on('blur', blurListener) .on('$md.pressdown', onPressDown) .on('$md.pressup', onPressUp) .on('$md.dragstart', onDragStart) @@ -138,7 +239,6 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi function updateAll() { refreshSliderDimensions(); ngModelRender(); - redrawTicks(); } setTimeout(updateAll, 0); @@ -161,6 +261,7 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi var min; var max; var step; + var round; function updateMin(value) { min = parseFloat(value); element.attr('aria-valuemin', value); @@ -173,7 +274,10 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi } function updateStep(value) { step = parseFloat(value); - redrawTicks(); + } + function updateRound(value) { + // Set max round digits to 6, after 6 the input uses scientific notation + round = minMaxValidator(parseInt(value), 0, 6); } function updateAriaDisabled(isDisabled) { element.attr('aria-disabled', !!isDisabled); @@ -184,7 +288,7 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi // which could quickly become a performance bottleneck. var tickCanvas, tickCtx; function redrawTicks() { - if (!angular.isDefined(attr.mdDiscrete)) return; + if (!discrete || isDisabledGetter()) return; if ( angular.isUndefined(step) ) return; if ( step <= 0 ) { @@ -198,22 +302,40 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi tickCanvas = angular.element('').css('position', 'absolute'); tickContainer.append(tickCanvas); - var trackTicksStyle = $window.getComputedStyle(tickContainer[0]); tickCtx = tickCanvas[0].getContext('2d'); - tickCtx.fillStyle = trackTicksStyle.backgroundColor || 'black'; } var dimensions = getSliderDimensions(); + + // If `dimensions` doesn't have height and width it might be the first attempt so we will refresh dimensions + if (dimensions && !dimensions.height && !dimensions.width) { + refreshSliderDimensions(); + dimensions = sliderDimensions; + } + 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); + var trackTicksStyle = $window.getComputedStyle(tickContainer[0]); + tickCtx.fillStyle = trackTicksStyle.color || 'black'; + + distance = Math.floor((vertical ? dimensions.height : dimensions.width) * (i / numSteps)); + + tickCtx.fillRect(vertical ? 0 : distance - 1, + vertical ? distance - 1 : 0, + vertical ? dimensions.width : 2, + vertical ? 2 : dimensions.height); } } + function clearTicks() { + if(tickCanvas && tickCtx) { + var dimensions = getSliderDimensions(); + tickCtx.clearRect(0, 0, dimensions.width, dimensions.height); + } + } /** * Refreshing Dimensions @@ -229,7 +351,7 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi } /** - * left/right arrow listener + * left/right/up/down arrow listener */ function keydownListener(ev) { if(element[0].hasAttribute('disabled')) { @@ -237,9 +359,9 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi } var changeAmount; - if (ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) { + if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.DOWN_ARROW : ev.keyCode === $mdConstant.KEY_CODE.LEFT_ARROW) { changeAmount = -step; - } else if (ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) { + } else if (vertical ? ev.keyCode === $mdConstant.KEY_CODE.UP_ARROW : ev.keyCode === $mdConstant.KEY_CODE.RIGHT_ARROW) { changeAmount = step; } if (changeAmount) { @@ -254,6 +376,29 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi } } + function mouseDownListener() { + redrawTicks(); + + scope.mouseActive = true; + wrapper.removeClass('md-focused'); + + $timeout(function() { + scope.mouseActive = false; + }, 100); + } + + function focusListener() { + if (scope.mouseActive === false) { + wrapper.addClass('md-focused'); + } + } + + function blurListener() { + wrapper.removeClass('md-focused'); + element.removeClass('_md-active'); + clearTicks(); + } + /** * ngModel setters and validators */ @@ -265,6 +410,8 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi ngModelCtrl.$viewValue = ngModelCtrl.$modelValue; } + ngModelCtrl.$viewValue = minMaxValidator(ngModelCtrl.$viewValue); + var percent = (ngModelCtrl.$viewValue - min) / (max - min); scope.modelValue = ngModelCtrl.$viewValue; element.attr('aria-valuenow', ngModelCtrl.$viewValue); @@ -272,16 +419,27 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi thumbText.text( ngModelCtrl.$viewValue ); } - function minMaxValidator(value) { + function minMaxValidator(value, minValue, maxValue) { if (angular.isNumber(value)) { - return Math.max(min, Math.min(max, value)); + minValue = angular.isNumber(minValue) ? minValue : min; + maxValue = angular.isNumber(maxValue) ? maxValue : max; + + return Math.max(minValue, Math.min(maxValue, value)); } } + function stepValidator(value) { if (angular.isNumber(value)) { var formattedValue = (Math.round((value - min) / step) * step + min); - // Format to 3 digits after the decimal point - fixes #2015. - return (Math.round(formattedValue * 1000) / 1000); + formattedValue = (Math.round(formattedValue * Math.pow(10, round)) / Math.pow(10, round)); + + if (containerCtrl && containerCtrl.fitInputWidthToTextLength){ + $mdUtil.debounce(function () { + containerCtrl.fitInputWidthToTextLength(formattedValue.toString().length); + }, 100)(); + } + + return formattedValue; } } @@ -290,23 +448,21 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi */ function setSliderPercent(percent) { - percent = clamp(percent); + percent = clamp(percent); - var percentStr = (percent * 100) + '%'; + var thumbPosition = (percent * 100) + '%'; - activeTrack.css('width', percentStr); - thumbContainer.css('left',percentStr); + thumbContainer.css(vertical ? 'bottom' : 'left', thumbPosition); + activeTrack.css(vertical ? 'height' : 'width', thumbPosition); - element.toggleClass('_md-min', percent === 0); - element.toggleClass('_md-max', percent === 1); + element.toggleClass('_md-min', percent === 0); + element.toggleClass('_md-max', percent === 1); } - /** * Slide listeners */ var isDragging = false; - var isDiscrete = angular.isDefined(attr.mdDiscrete); function onPressDown(ev) { if (isDisabledGetter()) return; @@ -315,7 +471,7 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi element[0].focus(); refreshSliderDimensions(); - var exactVal = percentToValue( positionToPercent( ev.pointer.x )); + var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x )); var closestVal = minMaxValidator( stepValidator(exactVal) ); scope.$apply(function() { setModelValue( closestVal ); @@ -325,9 +481,9 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi function onPressUp(ev) { if (isDisabledGetter()) return; - element.removeClass('_md-dragging _md-active'); + element.removeClass('_md-dragging'); - var exactVal = percentToValue( positionToPercent( ev.pointer.x )); + var exactVal = percentToValue( positionToPercent( vertical ? ev.pointer.y : ev.pointer.x )); var closestVal = minMaxValidator( stepValidator(exactVal) ); scope.$apply(function() { setModelValue(closestVal); @@ -337,6 +493,7 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi function onDragStart(ev) { if (isDisabledGetter()) return; isDragging = true; + ev.stopPropagation(); element.addClass('_md-dragging'); @@ -356,8 +513,8 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi function setSliderFromEvent(ev) { // While panning discrete, update only the // visual positioning but not the model value. - if ( isDiscrete ) adjustThumbPosition( ev.pointer.x ); - else doSlide( ev.pointer.x ); + if ( discrete ) adjustThumbPosition( vertical ? ev.pointer.y : ev.pointer.x ); + else doSlide( vertical ? ev.pointer.y : ev.pointer.x ); } /** @@ -391,12 +548,16 @@ function SliderDirective($$rAF, $window, $mdAria, $mdUtil, $mdConstant, $mdThemi } /** - * Convert horizontal position on slider to percentage value of offset from beginning... - * @param x + * Convert position on slider to percentage value of offset from beginning... + * @param position * @returns {number} */ - function positionToPercent( x ) { - return Math.max(0, Math.min(1, (x - sliderDimensions.left) / (sliderDimensions.width))); + function positionToPercent( position ) { + var offset = vertical ? sliderDimensions.top : sliderDimensions.left; + var size = vertical ? sliderDimensions.height : sliderDimensions.width; + var calc = (position - offset) / size; + + return Math.max(0, Math.min(1, vertical ? 1 - calc : calc)); } /** diff --git a/src/components/slider/slider.scss b/src/components/slider/slider.scss index 4475c61f235..0635329dcf1 100644 --- a/src/components/slider/slider.scss +++ b/src/components/slider/slider.scss @@ -1,17 +1,19 @@ $slider-background-color: rgb(200, 200, 200) !default; -$slider-height: 48px !default; +$slider-size: 48px !default; +$slider-min-size: 128px !default; $slider-track-height: 2px !default; -$slider-thumb-width: 32px !default; +$slider-thumb-width: 20px !default; $slider-thumb-height: $slider-thumb-width !default; -$slider-thumb-default-scale: 0.5 !default; -$slider-thumb-hover-scale: 0.6 !default; -$slider-thumb-focus-scale: 0.85 !default; -$slider-thumb-disabled-scale: 0.35 !default; -$slider-thumb-disabled-border: 6px !default; +$slider-thumb-default-scale: 0.7 !default; +$slider-thumb-hover-scale: 0.8 !default; +$slider-thumb-focus-scale: 1 !default; +$slider-thumb-disabled-scale: 0.5 !default; +$slider-thumb-disabled-border: 4px !default; +$slider-thumb-focus-duration: .7s !default; -$slider-focus-thumb-width: 48px !default; +$slider-focus-thumb-width: 34px !default; $slider-focus-thumb-height: $slider-focus-thumb-width !default; $slider-focus-ring-border-width: 3px !default; @@ -20,45 +22,76 @@ $slider-arrow-width: 28px !default; $slider-sign-height: 28px !default; $slider-sign-width: 28px !default; -$slider-sign-top: ($slider-height / 2) - ($slider-thumb-default-scale * $slider-thumb-height / 2) - ($slider-sign-height) - ($slider-arrow-height) + 8px !default; +$slider-sign-top: ($slider-size / 2) - ($slider-thumb-default-scale * $slider-thumb-height / 2) - ($slider-sign-height) - ($slider-arrow-height) + 10px !default; @keyframes sliderFocusThumb { 0% { + transform: scale($slider-thumb-default-scale); + } + 30% { + transform: scale($slider-thumb-focus-scale); + } + 100% { + transform: scale($slider-thumb-default-scale); + } +} + +@keyframes sliderDiscreteFocusThumb { + 0% { + transform: scale($slider-thumb-default-scale); + } + 50% { + transform: scale($slider-thumb-hover-scale); + } + 100% { + transform: scale(0); + } +} + +@keyframes sliderDiscreteFocusRing { + 0% { + transform: scale(0.7); opacity: 0; - transform: scale(0.0); } 50% { - transform: scale(1.0); + transform: scale(1); opacity: 1; } 100% { - opacity: 0; + transform: scale(0); } } @mixin slider-thumb-position($width: $slider-thumb-width, $height: $slider-thumb-height) { position: absolute; left: -$width / 2; - top: ($slider-height / 2) - ($height / 2); + top: ($slider-size / 2) - ($height / 2); width: $width; height: $height; border-radius: max($width, $height); } md-slider { - - height: $slider-height; + height: $slider-size; + min-width: $slider-min-size; position: relative; - display: block; margin-left: 4px; margin-right: 4px; padding: 0; + display: block; + flex-direction: row; *, *:after { box-sizing: border-box; } ._md-slider-wrapper { + outline: none; + width: 100%; + height: 100%; + } + + ._md-slider-content { position: relative; } @@ -68,7 +101,7 @@ md-slider { ._md-track-container { width: 100%; position: absolute; - top: ($slider-height / 2) - ($slider-track-height) / 2; + top: ($slider-size / 2) - ($slider-track-height) / 2; height: $slider-track-height; } ._md-track { @@ -78,7 +111,8 @@ md-slider { height: 100%; } ._md-track-fill { - transition: width 0.05s linear; + transition: all .4s cubic-bezier(.25,.8,.25,1); + transition-property: width, height; } ._md-track-ticks { position: absolute; @@ -87,10 +121,11 @@ md-slider { height: 100%; } ._md-track-ticks canvas { - // Restrict the width of the canvas so that ticks are rendered correctly + // Restrict the width and the height of the canvas so that ticks are rendered correctly // when parent elements are resized. Else, the position of the ticks might // be incorrect as we only update the canvas width attribute on window resize. width: 100%; + height: 100%; } /** @@ -101,14 +136,13 @@ md-slider { left: 0; top: 50%; transform: translate3d(-50%,-50%,0); - transition: left 0.1s linear; + transition: all .4s cubic-bezier(.25,.8,.25,1); + transition-property: left, bottom; } ._md-thumb { z-index: 1; - - // 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); + + @include slider-thumb-position($slider-thumb-width, $slider-thumb-height); // 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 @@ -116,17 +150,16 @@ md-slider { &: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-width: 3px; border-style: solid; + transition: inherit; } transform: scale($slider-thumb-default-scale); - transition: all 0.1s linear; + transition: all .4s cubic-bezier(.25,.8,.25,1); } /* The sign that's focused in discrete mode */ @@ -144,8 +177,8 @@ md-slider { height: $slider-sign-height; border-radius: max($slider-sign-height, $slider-sign-width); - transform: scale(0.4) translate3d(0,(-$slider-sign-top + 8) / 0.4,0); - transition: all 0.2s ease-in-out; + transform: scale(0.4) translate3d(0,(-$slider-sign-top + 10) / 0.4,0); + transition: all 0.3s $swift-ease-in-out-timing-function; /* The arrow pointing down under the sign */ &:after { @@ -161,7 +194,7 @@ md-slider { opacity: 0; transform: translate3d(0,-8px,0); - transition: all 0.2s ease-in-out; + transition: all 0.2s $swift-ease-in-out-timing-function; } ._md-thumb-text { @@ -174,18 +207,12 @@ md-slider { /** * The border/background that comes in when focused in non-discrete mode */ - ._md-focus-thumb { - @include slider-thumb-position($slider-focus-thumb-width, $slider-focus-thumb-height); - display: none; - opacity: 0; - background-color: #C0C0C0; - animation: sliderFocusThumb 0.4s linear; - } ._md-focus-ring { @include slider-thumb-position($slider-focus-thumb-width, $slider-focus-thumb-height); - transform: scale(0); - transition: all 0.2s linear; - opacity: 0.26; + transform: scale(.7); + opacity: 0; + // using a custom duration to match the spec example video + transition: all ($slider-thumb-focus-duration / 2) $swift-ease-in-out-timing-function; } ._md-disabled-thumb { @include slider-thumb-position( @@ -199,11 +226,6 @@ md-slider { } &._md-min { - ._md-thumb { - &:after { - background-color: white; - } - } ._md-sign { opacity: 0; } @@ -229,37 +251,50 @@ md-slider { } &:not([disabled]) { - &:hover { - ._md-thumb { + ._md-slider-wrapper { + ._md-thumb:hover { transform: scale($slider-thumb-hover-scale); } + + &.md-focused { + ._md-focus-ring { + transform: scale(1); + opacity: 1; + } + ._md-thumb { + animation: sliderFocusThumb $slider-thumb-focus-duration $swift-ease-in-out-timing-function; + } + } } - &:focus, &._md-active { - ._md-focus-thumb { - display: block; - } - ._md-focus-ring { - transform: scale(1); - } - ._md-thumb { - transform: scale($slider-thumb-focus-scale); + ._md-slider-wrapper { + ._md-thumb { + transform: scale($slider-thumb-focus-scale); + } } } } } &[md-discrete] { - /* Hide the focus thumb in discrete mode */ - ._md-focus-thumb, - ._md-focus-ring { - display: none; - } - &:not([disabled]) { - &:focus, + ._md-slider-wrapper { + &.md-focused { + ._md-focus-ring { + transform: scale(0); + animation: sliderDiscreteFocusRing .5s $swift-ease-in-out-timing-function; + } + ._md-thumb { + animation: sliderDiscreteFocusThumb .5s $swift-ease-in-out-timing-function; + } + } + } + ._md-slider-wrapper.md-focused, &._md-active { + ._md-thumb { + transform: scale(0); + } ._md-sign, ._md-sign:after { opacity: 1; @@ -267,14 +302,28 @@ md-slider { } } } + + &[disabled][readonly] { + ._md-thumb { + transform: scale(0); + } + ._md-sign, + ._md-sign:after { + opacity: 1; + transform: translate3d(0,0,0) scale(1.0); + } + } } &[disabled] { ._md-track-fill { display: none; } - ._md-sign { - display: none; + ._md-track-ticks { + opacity: 0; + } + &:not([readonly]) ._md-sign { + opacity: 0; } ._md-thumb { transform: scale($slider-thumb-disabled-scale); @@ -283,6 +332,148 @@ md-slider { display: block; } } + + &[md-vertical] { + flex-direction: column; + min-height: $slider-min-size; + min-width: 0; + + ._md-slider-wrapper { + flex: 1; + padding-top: 12px; + padding-bottom: 12px; + width: $slider-size; + align-self: center; + display: flex; + justify-content: center; + } + + ._md-track-container { + height: 100%; + width: $slider-track-height; + top: 0; + left: calc(50% - (#{$slider-track-height} / 2)); + } + + ._md-thumb-container { + top: auto; + margin-bottom: ($slider-size / 2) - ($slider-track-height) / 2; + left: calc(50% - 1px); + bottom: 0; + + ._md-thumb:after { + left: 1px; + } + + ._md-focus-ring { + left: -(($slider-focus-thumb-width / 2) - ($slider-track-height / 2)); + } + } + + ._md-track-fill { + bottom: 0; + } + + &[md-discrete] { + ._md-sign { + $sign-top: -($slider-sign-top / 2) + 1; + + left: -$slider-sign-height - 12; + top: $sign-top; + + transform: scale(0.4) translate3d((-$slider-sign-top + 10) / 0.4, 0 ,0); + + /* The arrow pointing left next the sign */ + &:after { + top: $sign-top; + left: 19px; + border-top: $slider-arrow-width / 2 solid transparent; + border-right: 0; + border-bottom: $slider-arrow-width / 2 solid transparent; + border-left-width: $slider-arrow-height; + border-left-style: solid; + + opacity: 0; + transform: translate3d(0,-8px,0); + transition: all 0.2s ease-in-out; + } + + ._md-thumb-text { + z-index: 1; + font-size: 12px; + font-weight: bold; + } + } + + &._md-active, + .md-focused, + &[disabled][readonly]{ + ._md-sign:after { + top: 0; + } + } + } + + &[disabled][readonly] { + ._md-thumb { + transform: scale(0); + } + ._md-sign, + ._md-sign:after { + opacity: 1; + transform: translate3d(0,0,0) scale(1.0); + } + } + } +} + +md-slider-container { + display: flex; + align-items: center; + flex-direction: row; + + $items-width: 25px; + $items-height: $items-width; + $items-margin: 16px; + + & > *:first-child, + & > *:last-child { + &:not(md-slider){ + min-width: $items-width; + max-width: ($items-width * 2) - 8; + height: $items-height; + transition: $swift-ease-out; + transition-property: color, max-width; + } + } + + & > *:first-child:not(md-slider) { + margin-right: $items-margin; + } + + & > *:last-child:not(md-slider) { + margin-left: $items-margin; + } + + &[md-vertical] { + flex-direction: column; + + & > *:first-child:not(md-slider), + & > *:last-child:not(md-slider) { + margin-right: 0; + margin-left: 0; + text-align: center; + } + } + + md-input-container { + input[type="number"] { + text-align: center; + padding-left: 15px; // size of arrows + height: $items-height * 2; + margin-top: -$items-height; + } + } } @media screen and (-ms-high-contrast: active) { diff --git a/src/components/slider/slider.spec.js b/src/components/slider/slider.spec.js index e4951831d5b..d24fb0f506c 100644 --- a/src/components/slider/slider.spec.js +++ b/src/components/slider/slider.spec.js @@ -23,13 +23,26 @@ describe('md-slider', function() { 'getBoundingClientRect' ).and.returnValue(angular.extend({ width: 100, + height: 100, left: 0, - right: 0 + right: 0, + bottom: 0, + top: 0 }, dimensions || {})); return slider; } + function setupContainer(attrs, sliderAttrs) { + return $compile('' + + '' + + '')(pageScope); + } + + function getWrapper(slider) { + return angular.element(slider[0].querySelector('._md-slider-wrapper')); + } + it('should not set model below the min', function() { var slider = setup('ng-model="value" min="0" max="100"'); pageScope.$apply('value = -50'); @@ -46,17 +59,19 @@ describe('md-slider', function() { var slider = setup('ng-model="value" min="0" max="100"'); pageScope.$apply('value = 50'); - slider.triggerHandler({type: '$md.pressdown', pointer: { x: 30 }}); - slider.triggerHandler({type: '$md.dragstart', pointer: { x: 30 }}); + var wrapper = getWrapper(slider); + + wrapper.triggerHandler({type: '$md.pressdown', pointer: { x: 30 }}); + wrapper.triggerHandler({type: '$md.dragstart', pointer: { x: 30 }}); $timeout.flush(); expect(pageScope.value).toBe(30); // When going past max, it should clamp to max. - slider.triggerHandler({type: '$md.drag', pointer: { x: 150 }}); + wrapper.triggerHandler({type: '$md.drag', pointer: { x: 150 }}); $timeout.flush(); expect(pageScope.value).toBe(100); - slider.triggerHandler({type: '$md.drag', pointer: { x: 50 }}); + wrapper.triggerHandler({type: '$md.drag', pointer: { x: 50 }}); $timeout.flush(); expect(pageScope.value).toBe(50); }); @@ -65,14 +80,16 @@ describe('md-slider', function() { var slider = setup('min="100" max="104" step="2" ng-model="model"'); pageScope.$apply('model = 100'); - slider.triggerHandler({ + var wrapper = getWrapper(slider); + + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.RIGHT_ARROW }); $timeout.flush(); expect(pageScope.model).toBe(102); - slider.triggerHandler({ + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.RIGHT_ARROW }); @@ -80,7 +97,7 @@ describe('md-slider', function() { expect(pageScope.model).toBe(104); // Stays at max. - slider.triggerHandler({ + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.RIGHT_ARROW }); @@ -92,14 +109,16 @@ describe('md-slider', function() { var slider = setup('min="100" max="104" step="2" ng-model="model"'); pageScope.$apply('model = 104'); - slider.triggerHandler({ + var wrapper = getWrapper(slider); + + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.LEFT_ARROW }); $timeout.flush(); expect(pageScope.model).toBe(102); - slider.triggerHandler({ + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.LEFT_ARROW }); @@ -107,7 +126,7 @@ describe('md-slider', function() { expect(pageScope.model).toBe(100); // Stays at min. - slider.triggerHandler({ + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.LEFT_ARROW }); @@ -117,22 +136,23 @@ describe('md-slider', function() { it('should update the thumb text', function() { var slider = setup('ng-model="value" md-discrete min="0" max="100" step="1"'); + var wrapper = getWrapper(slider); pageScope.$apply('value = 30'); expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('30'); - slider.triggerHandler({ + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.LEFT_ARROW }); $timeout.flush(); expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('29'); - slider.triggerHandler({type: '$md.pressdown', pointer: { x: 30 }}); + wrapper.triggerHandler({type: '$md.pressdown', pointer: { x: 30 }}); expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('30'); - slider.triggerHandler({type: '$md.dragstart', pointer: { x: 31 }}); - slider.triggerHandler({type: '$md.drag', pointer: { x: 31 }}); + wrapper.triggerHandler({type: '$md.dragstart', pointer: { x: 31 }}); + wrapper.triggerHandler({type: '$md.drag', pointer: { x: 31 }}); expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('31'); }); @@ -142,8 +162,9 @@ describe('md-slider', function() { }; var slider = setup('ng-model="value" min="0" max="100" ng-change="stayAt50()"'); + var wrapper = getWrapper(slider); - slider.triggerHandler({type: '$md.pressdown', pointer: { x: 30 }}); + wrapper.triggerHandler({type: '$md.pressdown', pointer: { x: 30 }}); $timeout.flush(); expect(pageScope.value).toBe(50); expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('50'); @@ -163,15 +184,16 @@ describe('md-slider', function() { it('should add aria attributes', function() { var slider = setup('min="100" max="104" step="2" ng-model="model"'); + var wrapper = getWrapper(slider); pageScope.$apply('model = 102'); - expect(slider.attr('role')).toEqual('slider'); + expect(wrapper.attr('role')).toEqual('slider'); expect(slider.attr('aria-valuemin')).toEqual('100'); expect(slider.attr('aria-valuemax')).toEqual('104'); expect(slider.attr('aria-valuenow')).toEqual('102'); - slider.triggerHandler({ + wrapper.triggerHandler({ type: 'keydown', keyCode: $mdConstant.KEY_CODE.LEFT_ARROW }); @@ -182,9 +204,10 @@ describe('md-slider', function() { it('should ignore pressdown events when disabled', function() { pageScope.isDisabled = true; var slider = setup('ng-disabled="isDisabled"'); + var wrapper = getWrapper(slider); // Doesn't add active class on pressdown when disabled - slider.triggerHandler({ + wrapper.triggerHandler({ type: '$md.pressdown', pointer: {} }); @@ -192,7 +215,7 @@ describe('md-slider', function() { // Doesn't remove active class up on pressup when disabled slider.addClass('_md-active'); - slider.triggerHandler({ + wrapper.triggerHandler({ type: '$md.pressup', pointer: {} }); @@ -201,29 +224,31 @@ describe('md-slider', function() { it('should disable via the `disabled` attribute', function() { var slider = setup('disabled'); + var wrapper = getWrapper(slider); // Check for disabled state by triggering the pressdown handler and asserting that // the slider is not active. - slider.triggerHandler({ + wrapper.triggerHandler({ type: '$md.pressdown', pointer: {} }); expect(slider).not.toHaveClass('_md-active'); }); - it('should add active class on pressdown and remove on pressup', function() { + it('should add active class on pressdown and remove on blur', function() { var slider = setup(); + var wrapper = getWrapper(slider); expect(slider).not.toHaveClass('_md-active'); - slider.triggerHandler({ + wrapper.triggerHandler({ type: '$md.pressdown', pointer: {} }); expect(slider).toHaveClass('_md-active'); - slider.triggerHandler({ - type: '$md.pressup', + wrapper.triggerHandler({ + type: 'blur', pointer: {} }); expect(slider).not.toHaveClass('_md-active'); @@ -231,22 +256,26 @@ describe('md-slider', function() { it('should add _md-min class only when at min value', function() { var slider = setup('ng-model="model" min="0" max="30"'); + var wrapper = getWrapper(slider); + pageScope.$apply('model = 0'); expect(slider).toHaveClass('_md-min'); - slider.triggerHandler({type: '$md.dragstart', pointer: {x: 0}}); - slider.triggerHandler({type: '$md.drag', pointer: {x: 10}}); + wrapper.triggerHandler({type: '$md.dragstart', pointer: {x: 0}}); + wrapper.triggerHandler({type: '$md.drag', pointer: {x: 10}}); $timeout.flush(); expect(slider).not.toHaveClass('_md-min'); }); it('should add _md-max class only when at max value', function() { var slider = setup('ng-model="model" min="0" max="30"'); + var wrapper = getWrapper(slider); + pageScope.$apply('model = 30'); expect(slider).toHaveClass('_md-max'); - slider.triggerHandler({type: '$md.dragstart', pointer: {x: 30}}); - slider.triggerHandler({type: '$md.drag', pointer: {x: 10}}); + wrapper.triggerHandler({type: '$md.dragstart', pointer: {x: 30}}); + wrapper.triggerHandler({type: '$md.drag', pointer: {x: 10}}); $timeout.flush(); expect(slider).not.toHaveClass('_md-max'); }); @@ -272,11 +301,13 @@ describe('md-slider', function() { var slider = setup('ng-model="value" min="' + min + '" max="' + max + '" step="' + step + '"'); pageScope.$apply('value = 0.5'); + var wrapper = getWrapper(slider); + return { drag : function simulateDrag(drag) { - slider.triggerHandler({type: '$md.pressdown', pointer: drag }); - slider.triggerHandler({type: '$md.dragstart', pointer: drag }); + wrapper.triggerHandler({type: '$md.pressdown', pointer: drag }); + wrapper.triggerHandler({type: '$md.dragstart', pointer: drag }); $timeout.flush(); } @@ -285,14 +316,251 @@ describe('md-slider', function() { }); + describe('vertical', function () { + it('should set model on press', function() { + var slider = setup('md-vertical ng-model="value" min="0" max="100"'); + pageScope.$apply('value = 50'); + + var wrapper = getWrapper(slider); + + wrapper.triggerHandler({type: '$md.pressdown', pointer: { y: 70 }}); + wrapper.triggerHandler({type: '$md.dragstart', pointer: { y: 70 }}); + $timeout.flush(); + expect(pageScope.value).toBe(30); + + // When going past max, it should clamp to max. + wrapper.triggerHandler({type: '$md.drag', pointer: { y: 0 }}); + $timeout.flush(); + expect(pageScope.value).toBe(100); + + wrapper.triggerHandler({type: '$md.drag', pointer: { y: 50 }}); + $timeout.flush(); + expect(pageScope.value).toBe(50); + }); + + it('should increment model on up arrow', function() { + var slider = setup('md-vertical min="100" max="104" step="2" ng-model="model"'); + pageScope.$apply('model = 100'); + + var wrapper = getWrapper(slider); + + wrapper.triggerHandler({ + type: 'keydown', + keyCode: $mdConstant.KEY_CODE.UP_ARROW + }); + $timeout.flush(); + expect(pageScope.model).toBe(102); + + wrapper.triggerHandler({ + type: 'keydown', + keyCode: $mdConstant.KEY_CODE.UP_ARROW + }); + $timeout.flush(); + expect(pageScope.model).toBe(104); + + // Stays at max. + wrapper.triggerHandler({ + type: 'keydown', + keyCode: $mdConstant.KEY_CODE.UP_ARROW + }); + $timeout.flush(); + expect(pageScope.model).toBe(104); + }); + + it('should decrement model on down arrow', function() { + var slider = setup('md-vertical min="100" max="104" step="2" ng-model="model"'); + pageScope.$apply('model = 104'); + + var wrapper = getWrapper(slider); + + wrapper.triggerHandler({ + type: 'keydown', + keyCode: $mdConstant.KEY_CODE.DOWN_ARROW + }); + $timeout.flush(); + expect(pageScope.model).toBe(102); + + wrapper.triggerHandler({ + type: 'keydown', + keyCode: $mdConstant.KEY_CODE.DOWN_ARROW + }); + $timeout.flush(); + expect(pageScope.model).toBe(100); + + // Stays at min. + wrapper.triggerHandler({ + type: 'keydown', + keyCode: $mdConstant.KEY_CODE.DOWN_ARROW + }); + $timeout.flush(); + expect(pageScope.model).toBe(100); + }); + + it('should update the thumb text', function() { + var slider = setup('md-vertical ng-model="value" md-discrete min="0" max="100" step="1"'); + var wrapper = getWrapper(slider); + + pageScope.$apply('value = 30'); + expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('30'); + + wrapper.triggerHandler({ + type: 'keydown', + keyCode: $mdConstant.KEY_CODE.DOWN_ARROW + }); + $timeout.flush(); + expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('29'); + + wrapper.triggerHandler({type: '$md.pressdown', pointer: { y: 70 }}); + expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('30'); + + wrapper.triggerHandler({type: '$md.dragstart', pointer: { y: 93 }}); + wrapper.triggerHandler({type: '$md.drag', pointer: { y: 93 }}); + expect(slider[0].querySelector('._md-thumb-text').textContent).toBe('7'); + }); + + it('should add _md-min class only when at min value', function() { + var slider = setup('md-vertical ng-model="model" min="0" max="30"'); + var wrapper = getWrapper(slider); + + pageScope.$apply('model = 0'); + expect(slider).toHaveClass('_md-min'); + + wrapper.triggerHandler({type: '$md.dragstart', pointer: {y: 0}}); + wrapper.triggerHandler({type: '$md.drag', pointer: {y: 10}}); + $timeout.flush(); + expect(slider).not.toHaveClass('_md-min'); + }); + + it('should add _md-max class only when at max value', function() { + var slider = setup('md-vertical ng-model="model" min="0" max="30"'); + var wrapper = getWrapper(slider); + + pageScope.$apply('model = 30'); + expect(slider).toHaveClass('_md-max'); + + wrapper.triggerHandler({type: '$md.dragstart', pointer: {y: 30}}); + wrapper.triggerHandler({type: '$md.drag', pointer: {y: 10}}); + $timeout.flush(); + expect(slider).not.toHaveClass('_md-max'); + }); + + it('should increment at a predictable step', function() { + + buildSlider(0.1, 0, 1).drag({y:30}); + expect(pageScope.value).toBe(0.7); + + buildSlider(0.25, 0, 1).drag({y:45}); + expect(pageScope.value).toBe(0.5); + + buildSlider(0.25, 0, 1).drag({y:75}); + expect(pageScope.value).toBe(0.25); + + buildSlider(1, 0, 100).drag({y:10}); + expect(pageScope.value).toBe(90); + + buildSlider(20, 5, 45).drag({y:50}); + expect(pageScope.value).toBe(25); + + function buildSlider(step, min, max) { + var slider = setup('md-vertical ng-model="value" min="' + min + '" max="' + max + '" step="' + step + '"'); + pageScope.$apply('value = 0.5'); + + var wrapper = getWrapper(slider); + + return { + drag : function simulateDrag(drag) { + + wrapper.triggerHandler({type: '$md.pressdown', pointer: drag }); + wrapper.triggerHandler({type: '$md.dragstart', pointer: drag }); + + $timeout.flush(); + } + }; + } + + }); + }); + + describe('slider container', function () { + it('should disable via the `disabled` attribute', function() { + var container = setupContainer('disabled="disabled"'); + var slider = angular.element(container[0].querySelector('md-slider')); + var wrapper = getWrapper(container); + + // Check for disabled state by triggering the pressdown handler and asserting that + // the slider is not active. + wrapper.triggerHandler({ + type: '$md.pressdown', + pointer: {} + }); + expect(slider).not.toHaveClass('_md-active'); + }); + + it('should disable via the `ngDisabled` attribute', function() { + var container = setupContainer('ng-disabled="isDisabled"'); + var slider = angular.element(container[0].querySelector('md-slider')); + var wrapper = getWrapper(container); + + // Check for disabled state by triggering the pressdown handler and asserting that + // the slider is not active. + wrapper.triggerHandler({ + type: '$md.pressdown', + pointer: {} + }); + expect(slider).toHaveClass('_md-active'); + + // Removing focus from the slider + wrapper.triggerHandler({ + type: 'blur', + pointer: {} + }); + + pageScope.$apply('isDisabled = true'); + + wrapper.triggerHandler({ + type: '$md.pressdown', + pointer: {} + }); + expect(slider).not.toHaveClass('_md-active'); + }); + + it('should disable related inputs', inject(function($compile) { + var container = setupContainer('ng-disabled="isDisabled"'); + var slider = angular.element(container[0].querySelector('md-slider')); + + var inputContainer = $compile('')(pageScope); + var input = angular.element(inputContainer[0].querySelector('input')); + + container.append(input); + + expect(input.attr('disabled')).toEqual(undefined); + + pageScope.$apply('isDisabled = true'); + + expect(input.attr('disabled')).toEqual('disabled'); + })); + + }); + it('should set a default tabindex', function() { var slider = setup(); - expect(slider.attr('tabindex')).toBe('0'); + var wrapper = getWrapper(slider); + + expect(wrapper.attr('tabindex')).toBe('0'); + }); + + it('should set a -1 tabindex to disabled slider', function() { + var slider = setup('ng-disabled="isDisabled"'); + var wrapper = getWrapper(slider); + + expect(wrapper.attr('tabindex')).toBe('-1'); }); it('should not overwrite tabindex attribute', function() { var slider = setup('tabindex="2"'); - expect(slider.attr('tabindex')).toBe('2'); + var wrapper = getWrapper(slider); + + expect(wrapper.attr('tabindex')).toBe('2'); }); });